diff --git a/oss-licenses-plugin/src/main/groovy/com/google/android/gms/oss/licenses/plugin/LicensesTask.groovy b/oss-licenses-plugin/src/main/groovy/com/google/android/gms/oss/licenses/plugin/LicensesTask.groovy index e3cdd979..61733071 100644 --- a/oss-licenses-plugin/src/main/groovy/com/google/android/gms/oss/licenses/plugin/LicensesTask.groovy +++ b/oss-licenses-plugin/src/main/groovy/com/google/android/gms/oss/licenses/plugin/LicensesTask.groovy @@ -32,6 +32,7 @@ import org.gradle.api.tasks.TaskAction import org.slf4j.LoggerFactory import java.util.zip.ZipEntry +import java.util.zip.ZipException import java.util.zip.ZipFile /** @@ -46,18 +47,13 @@ abstract class LicensesTask extends DefaultTask { private static final String UTF_8 = "UTF-8" private static final byte[] LINE_SEPARATOR = System .getProperty("line.separator").getBytes(UTF_8) - private static final int GRANULAR_BASE_VERSION = 14 - private static final String GOOGLE_PLAY_SERVICES_GROUP = - "com.google.android.gms" - private static final String LICENSE_ARTIFACT_SUFFIX = "-license" - private static final String FIREBASE_GROUP = "com.google.firebase" private static final String FAIL_READING_LICENSES_ERROR = "Failed to read license text." private static final logger = LoggerFactory.getLogger(LicensesTask.class) protected int start = 0 - protected Set googleServiceLicenses = [] + protected Set embeddedLicenses = [] protected Map licensesMap = [:] protected Map licenseOffsets = [:] protected static final String ABSENT_DEPENDENCY_KEY = "Debug License Info" @@ -126,22 +122,18 @@ abstract class LicensesTask extends DefaultTask { addDebugLicense() } else { for (artifactInfo in artifactInfoSet) { - if (isGoogleServices(artifactInfo.group)) { - // Add license info for google-play-services itself - if (!artifactInfo.name.endsWith(LICENSE_ARTIFACT_SUFFIX)) { - addLicensesFromPom(pomMap, artifactInfo) - } - // Add transitive licenses info for google-play-services. For - // post-granular versions, this is located in the artifact - // itself, whereas for pre-granular versions, this information - // is located at the complementary license artifact as a runtime - // dependency. - if (isGranularVersion(artifactInfo.version) || artifactInfo.name.endsWith(LICENSE_ARTIFACT_SUFFIX)) { - addGooglePlayServiceLicenses(libraryMap, artifactInfo) - } - } else { + // 1. Extract licenses from POM for all artifacts, except for side-car license artifacts. + // Artifacts named "*-license" are containers for license data and shouldn't have + // their own entry in the attribution list. + if (!artifactInfo.name.endsWith("-license")) { addLicensesFromPom(pomMap, artifactInfo) } + + // 2. For any artifact, try to extract embedded licenses if they exist. + File libraryFile = libraryMap.get(artifactInfo.toString()) + if (libraryFile != null && libraryFile.exists()) { + addEmbeddedLicenses(libraryFile) + } } } @@ -181,59 +173,46 @@ abstract class LicensesTask extends DefaultTask { } } - protected static boolean isGoogleServices(String group) { - return (GOOGLE_PLAY_SERVICES_GROUP.equalsIgnoreCase(group) - || FIREBASE_GROUP.equalsIgnoreCase(group)) - } - - protected static boolean isGranularVersion(String version) { - String[] versions = version.split("\\.") - return (versions.length > 0 - && Integer.valueOf(versions[0]) >= GRANULAR_BASE_VERSION) - } - - protected void addGooglePlayServiceLicenses(Map libraryMap, ArtifactInfo artifactInfo) { - File libraryFile = libraryMap.get(artifactInfo.toString()) - if (libraryFile == null || !libraryFile.exists()) { - logger.warn("Unable to find Google Play Services Artifact for $artifactInfo") - return - } - addGooglePlayServiceLicenses(libraryFile) - } - - protected void addGooglePlayServiceLicenses(File artifactFile) { - ZipFile licensesZip = new ZipFile(artifactFile) - - ZipEntry jsonFile = licensesZip.getEntry("third_party_licenses.json") - ZipEntry txtFile = licensesZip.getEntry("third_party_licenses.txt") + protected void addEmbeddedLicenses(File artifactFile) { + try { + new ZipFile(artifactFile).withCloseable { licensesZip -> + ZipEntry jsonFile = licensesZip.getEntry("third_party_licenses.json") + ZipEntry txtFile = licensesZip.getEntry("third_party_licenses.txt") - if (!jsonFile || !txtFile) { - return - } + if (!jsonFile || !txtFile) { + return + } - JsonSlurper jsonSlurper = new JsonSlurper() - Object licensesObj = licensesZip.getInputStream(jsonFile).withCloseable { - jsonSlurper.parse(it) - } - if (licensesObj == null) { - return - } + JsonSlurper jsonSlurper = new JsonSlurper() + Object licensesObj = licensesZip.getInputStream(jsonFile).withCloseable { + jsonSlurper.parse(it) + } + if (licensesObj == null) { + return + } - for (entry in licensesObj) { - String key = entry.key - int startValue = entry.value.start - int lengthValue = entry.value.length - - if (!googleServiceLicenses.contains(key)) { - licensesZip.getInputStream(txtFile).withCloseable { - byte[] content = getBytesFromInputStream( - it, - startValue, - lengthValue) - googleServiceLicenses.add(key) - appendDependency(key, content) + for (entry in licensesObj) { + String key = entry.key + int startValue = entry.value.start + int lengthValue = entry.value.length + + if (!embeddedLicenses.contains(key)) { + licensesZip.getInputStream(txtFile).withCloseable { + byte[] content = getBytesFromInputStream( + it, + startValue, + lengthValue) + embeddedLicenses.add(key) + appendDependency(key, content) + } + } } } + } catch (ZipException e) { + // Not a zip file, or malformed. Skip. + logger.debug("Failed to open $artifactFile as a zip file: ${e.message}") + } catch (IOException e) { + logger.warn("Failed to read embedded licenses from $artifactFile: ${e.message}") } } diff --git a/oss-licenses-plugin/src/test/java/com/google/android/gms/oss/licenses/plugin/GoogleServicesLicenseTest.java b/oss-licenses-plugin/src/test/java/com/google/android/gms/oss/licenses/plugin/GoogleServicesLicenseTest.java deleted file mode 100644 index b77295a5..00000000 --- a/oss-licenses-plugin/src/test/java/com/google/android/gms/oss/licenses/plugin/GoogleServicesLicenseTest.java +++ /dev/null @@ -1,51 +0,0 @@ -/** - * Copyright 2018 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except - * in compliance with the License. You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software distributed under the License - * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express - * or implied. See the License for the specific language governing permissions and limitations under - * the License. - */ - -package com.google.android.gms.oss.licenses.plugin; - -import static org.junit.Assert.assertEquals; - -import java.util.Arrays; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.junit.runners.Parameterized; -import org.junit.runners.Parameterized.Parameter; -import org.junit.runners.Parameterized.Parameters; - -/** - * Test for {@link LicensesTask#isGoogleServices(String)} - */ -@RunWith(Parameterized.class) -public class GoogleServicesLicenseTest { - - @Parameter(0) - public String inputGroup; - @Parameter(1) - public Boolean expectedResult; - - @Parameters - public static Iterable data() { - return Arrays.asList( - new Object[][]{ - {"com.google.android.gms", true}, - {"com.google.firebase", true}, - {"com.example", false}, - }); - } - - @Test - public void test() { - assertEquals(expectedResult, LicensesTask.isGoogleServices(inputGroup)); - } -} diff --git a/oss-licenses-plugin/src/test/java/com/google/android/gms/oss/licenses/plugin/LicensesTaskTest.java b/oss-licenses-plugin/src/test/java/com/google/android/gms/oss/licenses/plugin/LicensesTaskTest.java index 7063760e..253b276f 100644 --- a/oss-licenses-plugin/src/test/java/com/google/android/gms/oss/licenses/plugin/LicensesTaskTest.java +++ b/oss-licenses-plugin/src/test/java/com/google/android/gms/oss/licenses/plugin/LicensesTaskTest.java @@ -87,18 +87,6 @@ public void testInitOutputDir() throws IOException { assertEquals(0, Files.size(licensesMetadata.toPath())); } - @Test - public void testIsGranularVersion_True() { - String versionTrue = "14.6.0"; - assertTrue(LicensesTask.isGranularVersion(versionTrue)); - } - - @Test - public void testIsGranularVersion_False() { - String versionFalse = "11.4.0"; - assertFalse(LicensesTask.isGranularVersion(versionFalse)); - } - @Test public void testAddLicensesFromPom() throws IOException { File deps1 = getResourceFile("dependencies/groupA/deps1.pom"); @@ -222,28 +210,29 @@ public void testGetBytesFromInputStream_specialCharacters() { } @Test - public void testAddGooglePlayServiceLicenses() throws IOException { + public void testAddEmbeddedLicenses() throws IOException { File tempOutput = temporaryFolder.newFolder(); tempOutput.mkdirs(); - createLicenseZip(tempOutput.getPath() + "play-services-foo-license.aar"); - File artifact = new File(tempOutput.getPath() + "play-services-foo-license.aar"); + String artifactName = "test-artifact.aar"; + createLicenseZip(tempOutput.getPath() + "/" + artifactName); + File artifact = new File(tempOutput.getPath() + "/" + artifactName); licensesTask.initOutputDir(); - licensesTask.addGooglePlayServiceLicenses(artifact); + licensesTask.addEmbeddedLicenses(artifact); String content = new String(Files.readAllBytes(licensesTask.getLicenses().toPath()), UTF_8); String expected = "safeparcel" + LINE_BREAK + "JSR 305" + LINE_BREAK; assertEquals(expected, content); - assertThat(licensesTask.googleServiceLicenses.size(), is(2)); - assertTrue(licensesTask.googleServiceLicenses.contains("safeparcel")); - assertTrue(licensesTask.googleServiceLicenses.contains("JSR 305")); + assertThat(licensesTask.embeddedLicenses.size(), is(2)); + assertTrue(licensesTask.embeddedLicenses.contains("safeparcel")); + assertTrue(licensesTask.embeddedLicenses.contains("JSR 305")); assertThat(licensesTask.licensesMap.size(), is(2)); assertTrue(licensesTask.licensesMap.containsKey("safeparcel")); assertTrue(licensesTask.licensesMap.containsKey("JSR 305")); } @Test - public void testAddGooglePlayServiceLicenses_withoutDuplicate() throws IOException { + public void testAddEmbeddedLicenses_withoutDuplicate() throws IOException { File groupC = temporaryFolder.newFolder(); groupC.mkdirs(); createLicenseZip(groupC.getPath() + "/play-services-foo-license.aar"); @@ -255,20 +244,106 @@ public void testAddGooglePlayServiceLicenses_withoutDuplicate() throws IOExcepti File artifactBar = new File(groupD.getPath() + "/play-services-bar-license.aar"); licensesTask.initOutputDir(); - licensesTask.addGooglePlayServiceLicenses(artifactFoo); - licensesTask.addGooglePlayServiceLicenses(artifactBar); + licensesTask.addEmbeddedLicenses(artifactFoo); + licensesTask.addEmbeddedLicenses(artifactBar); String content = new String(Files.readAllBytes(licensesTask.getLicenses().toPath()), UTF_8); String expected = "safeparcel" + LINE_BREAK + "JSR 305" + LINE_BREAK; assertEquals(expected, content); - assertThat(licensesTask.googleServiceLicenses.size(), is(2)); - assertTrue(licensesTask.googleServiceLicenses.contains("safeparcel")); - assertTrue(licensesTask.googleServiceLicenses.contains("JSR 305")); + assertThat(licensesTask.embeddedLicenses.size(), is(2)); + assertTrue(licensesTask.embeddedLicenses.contains("safeparcel")); + assertTrue(licensesTask.embeddedLicenses.contains("JSR 305")); assertThat(licensesTask.licensesMap.size(), is(2)); assertTrue(licensesTask.licensesMap.containsKey("safeparcel")); assertTrue(licensesTask.licensesMap.containsKey("JSR 305")); } + @Test + public void action_nonGmsGranularLibrary_extractsTransitiveLicenses() throws Exception { + // 1. Setup a non-GMS granular artifact + File artifactDir = temporaryFolder.newFolder("artifacts"); + String gav = "com.example:granular-lib:1.0.0"; + String artifactName = "granular-lib-1.0.0.aar"; + File artifactFile = new File(artifactDir, artifactName); + createLicenseZip(artifactFile.getAbsolutePath()); + + // 2. Setup POM file (empty or with its own license) + File pomDir = temporaryFolder.newFolder("poms"); + File pomFile = new File(pomDir, "granular-lib-1.0.0.pom"); + try (FileWriter writer = new FileWriter(pomFile)) { + writer.write("Granular LibApache 2.0http://www.apache.org/licenses/LICENSE-2.0"); + } + + // 3. Setup dependencies.json + File dependenciesJson = temporaryFolder.newFile("dependencies.json"); + ArtifactInfo[] artifactInfoArray = new ArtifactInfo[] { + new ArtifactInfo("com.example", "granular-lib", "1.0.0") + }; + Gson gson = new Gson(); + try (FileWriter writer = new FileWriter(dependenciesJson)) { + gson.toJson(artifactInfoArray, writer); + } + + // 4. Configure task inputs + licensesTask.getDependenciesJson().set(dependenciesJson); + licensesTask.getLibraryFilesByGav().put(gav, artifactFile); + licensesTask.getPomFilesByGav().put(gav, pomFile); + + // 5. Execute action + licensesTask.action(); + + // 6. Verify results + String content = new String(Files.readAllBytes(licensesTask.getLicenses().toPath()), UTF_8); + + // It should contain BOTH the POM license and the granular transitive licenses + assertTrue("Should contain POM license", content.contains("http://www.apache.org/licenses/LICENSE-2.0")); + assertTrue("Should contain Granular license 'safeparcel'", content.contains("safeparcel")); + assertTrue("Should contain Granular license 'JSR 305'", content.contains("JSR 305")); + } + + @Test + public void action_licenseSidecarArtifact_skipsPomButExtractsEmbedded() throws Exception { + // 1. Setup a side-car license artifact + File artifactDir = temporaryFolder.newFolder("sidecar_artifacts"); + String gav = "com.example:sidecar-license:1.0.0"; + String artifactName = "sidecar-license-1.0.0.aar"; + File artifactFile = new File(artifactDir, artifactName); + createLicenseZip(artifactFile.getAbsolutePath()); + + // 2. Setup POM file with a license URL (which should be ignored) + File pomDir = temporaryFolder.newFolder("sidecar_poms"); + File pomFile = new File(pomDir, "sidecar-license-1.0.0.pom"); + try (FileWriter writer = new FileWriter(pomFile)) { + writer.write("Sidecar LicenseIgnored POM Licensehttp://ignored-pom-url.com"); + } + + // 3. Setup dependencies.json + File dependenciesJson = temporaryFolder.newFile("sidecar_dependencies.json"); + ArtifactInfo[] artifactInfoArray = new ArtifactInfo[] { + new ArtifactInfo("com.example", "sidecar-license", "1.0.0") + }; + Gson gson = new Gson(); + try (FileWriter writer = new FileWriter(dependenciesJson)) { + gson.toJson(artifactInfoArray, writer); + } + + // 4. Configure task inputs + licensesTask.getDependenciesJson().set(dependenciesJson); + licensesTask.getLibraryFilesByGav().put(gav, artifactFile); + licensesTask.getPomFilesByGav().put(gav, pomFile); + + // 5. Execute action + licensesTask.action(); + + // 6. Verify results + String content = new String(Files.readAllBytes(licensesTask.getLicenses().toPath()), UTF_8); + + // It should NOT contain the POM license but SHOULD contain the granular transitive licenses + assertFalse("Should NOT contain POM license for sidecar", content.contains("http://ignored-pom-url.com")); + assertTrue("Should contain Granular license 'safeparcel'", content.contains("safeparcel")); + assertTrue("Should contain Granular license 'JSR 305'", content.contains("JSR 305")); + } + private void createLicenseZip(String name) throws IOException { File zipFile = new File(name); ZipOutputStream output = new ZipOutputStream(new FileOutputStream(zipFile));