Skip to content
This repository was archived by the owner on Nov 16, 2023. It is now read-only.

Commit b453795

Browse files
authored
fix(web-compliance): include missing dependencies in (manual) NOTICE.html (#45)
#### Description of changes In an earlier PR, we added a task to have Component Governance automatically generate NOTICE.html files for us. Unfortunately, there are several problems that block this approach from being accurate: 1. We have some cases where we use code that Android Studio generated, which means we need an attribution for The Android Open Source Project which Gradle/Component Governance doesn't know about 2. Component Governance will only include "supported components" in NOTICE files, which excludes anything from a cgmanifest file and notably excludes `androidx.annotation`, which is a Maven dependency not present in Maven Central 3. Component Governance doesn't have a deterministic way to tell whether a given dependency is a devDependency or not, so it is conservative and always assumes "real dependency". Unfortunately, this means that every time a devDependency is bumped by dependabot, we need to remember to go manually configure CG to omit the new version, or it will start showing up in NOTICE files We can sort of work around 1 with a cgmanifest file, but it immediately runs into 2. We don't have workarounds for 2 and 3. Until we do, we have to go back to manually curating NOTICE.html. This PR does that, but extends and automates the use of the `list-release-dependencies` script into something that verifies that the lockfile and NOTICE.html are in sync as a condition of PR builds passing, to avoid the case where Dependabot updates a version of something important and we forget to update NOTICE.html. It also adds the necessary copyright header for AOSP to the file in question that triggers us to include it. #### Pull request checklist <!-- If a checklist item is not applicable to this change, write "n/a" in the checkbox --> - [n/a] Addresses an existing issue: #0000 - [n/a] Added/updated relevant unit test(s) - [x] Ran `./gradlew fastpass` from `AccessibilityInsightsForAndroidService` - [x] PR title _AND_ final merge commit title both start with a semantic tag (`fix:`, `chore:`, `feat(feature-name):`, `refactor:`).
1 parent dae9e3d commit b453795

File tree

8 files changed

+796
-78
lines changed

8 files changed

+796
-78
lines changed

AccessibilityInsightsForAndroidService/app/gradle.lockfile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ com.android.tools.analytics-library:protos:27.0.1=lintClassPath
1010
com.android.tools.analytics-library:shared:27.0.1=lintClassPath
1111
com.android.tools.analytics-library:tracker:27.0.1=lintClassPath
1212
com.android.tools.build:aapt2-proto:0.4.0=lintClassPath
13+
com.android.tools.build:aapt2:4.0.1-6197926=_internal_aapt2_binary
1314
com.android.tools.build:apksig:4.0.1=lintClassPath
1415
com.android.tools.build:apkzlib:4.0.1=lintClassPath
1516
com.android.tools.build:builder-model:4.0.1=lintClassPath

AccessibilityInsightsForAndroidService/app/src/main/java/com/microsoft/accessibilityinsightsforandroidservice/AccessibilityNodeInfoSorter.java

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,19 @@
1-
// Copyright (c) Microsoft Corporation.
1+
// Portions Copyright (c) Microsoft Corporation.
22
// Licensed under the MIT License.
3+
//
4+
// Copyright (C) 2020 The Android Open Source Project
5+
//
6+
// Licensed under the Apache License, Version 2.0 (the "License");
7+
// you may not use this file except in compliance with the License.
8+
// You may obtain a copy of the License at
9+
//
10+
// http://www.apache.org/licenses/LICENSE-2.0
11+
//
12+
// Unless required by applicable law or agreed to in writing, software
13+
// distributed under the License is distributed on an "AS IS" BASIS,
14+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
// See the License for the specific language governing permissions and
16+
// limitations under the License.
317

418
package com.microsoft.accessibilityinsightsforandroidservice;
519

AccessibilityInsightsForAndroidService/build.gradle

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,8 @@ spotless {
3838
}
3939
java {
4040
target '**/*.java'
41-
// This file has the APACHE license header, which we need to keep
42-
targetExclude '**/AccessibilityInsightsForAndroidService.java'
41+
// These files have an APACHE license header, which we need to keep
42+
targetExclude '**/AccessibilityInsightsForAndroidService.java','**/AccessibilityNodeInfoSorter.java'
4343
licenseHeader '// Copyright (c) Microsoft Corporation.\n// Licensed under the MIT License.\n\n'
4444
}
4545
}

AccessibilityInsightsForAndroidService/list-release-dependencies.js

Lines changed: 0 additions & 45 deletions
This file was deleted.

NOTICE.html

Lines changed: 642 additions & 0 deletions
Large diffs are not rendered by default.

pipeline/release-build.yaml

Lines changed: 9 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -101,25 +101,22 @@ jobs:
101101

102102
# terms artifact
103103

104+
- task: ComponentGovernanceComponentDetection@0
105+
displayName: '(terms) dependency detection (Component Governance)'
106+
timeoutInMinutes: 5
107+
108+
- script: node $(system.defaultWorkingDirectory)/pipeline/verify-notice-contents.js
109+
displayName: (terms) verify NOTICE.html content matches lockfile
110+
104111
- task: CopyFiles@2
105-
displayName: (terms) copy LICENSE to artifact staging
112+
displayName: (terms) copy LICENSE and NOTICE.html to artifact staging
106113
inputs:
107114
contents: |
108115
LICENSE
116+
NOTICE.html
109117
sourceFolder: '$(system.defaultWorkingDirectory)'
110118
targetFolder: '$(build.artifactstagingdirectory)/terms'
111119

112-
- task: ComponentGovernanceComponentDetection@0
113-
displayName: '(terms) dependency detection (Component Governance)'
114-
timeoutInMinutes: 5
115-
116-
- task: msospo.ospo-extension.8d7f9abb-6896-461d-9e25-4f74ed65ddb2.notice@0
117-
displayName: '(terms) generate NOTICE.html file'
118-
inputs:
119-
outputfile: '$(build.artifactstagingdirectory)/terms/NOTICE.html'
120-
outputformat: html
121-
timeoutInMinutes: 5
122-
123120
- task: PublishPipelineArtifact@1
124121
displayName: (terms) publish artifact
125122
inputs:

pipeline/validation-build.yaml

Lines changed: 21 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -2,25 +2,28 @@
22
# Licensed under the MIT License.
33

44
trigger:
5-
- master
5+
- master
66

77
jobs:
8-
- job: 'gradlew_build'
8+
- job: 'gradlew_build'
99

10-
pool:
11-
vmImage: 'windows-latest'
10+
pool:
11+
vmImage: 'windows-latest'
1212

13-
steps:
14-
- task: Gradle@2
15-
displayName: build and test dev ai-android service
16-
inputs:
17-
workingDirectory: '$(system.defaultWorkingDirectory)/AccessibilityInsightsForAndroidService'
18-
gradleWrapperFile: 'AccessibilityInsightsForAndroidService/gradlew'
19-
gradleOptions: '-Xmx3072m'
20-
options: -S
21-
javaHomeOption: 'JDKVersion'
22-
jdkVersionOption: '1.8'
23-
jdkArchitectureOption: 'x64'
24-
publishJUnitResults: true
25-
testResultsFiles: '**/TEST-*.xml'
26-
tasks: 'build'
13+
steps:
14+
- task: Gradle@2
15+
displayName: build and test dev ai-android service
16+
inputs:
17+
workingDirectory: '$(system.defaultWorkingDirectory)/AccessibilityInsightsForAndroidService'
18+
gradleWrapperFile: 'AccessibilityInsightsForAndroidService/gradlew'
19+
gradleOptions: '-Xmx3072m'
20+
options: -S
21+
javaHomeOption: 'JDKVersion'
22+
jdkVersionOption: '1.8'
23+
jdkArchitectureOption: 'x64'
24+
publishJUnitResults: true
25+
testResultsFiles: '**/TEST-*.xml'
26+
tasks: 'build'
27+
28+
- script: node $(system.defaultWorkingDirectory)/pipeline/verify-notice-contents.js
29+
displayName: verify NOTICE.html content matches lockfile

pipeline/verify-notice-contents.js

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
// USAGE:
2+
//
3+
// node verify-notice-contents.js
4+
//
5+
// Verifies that gradle.lockfile and NOTICE.html are in sync. Exits with non-zero if NOTICE.html
6+
// contains any listings that gradle.lockfile doesn't suggest are required OR if NOTICE.html is
7+
// missing any listings that gradle.lockfile suggests are required.
8+
9+
const fs = require('fs');
10+
const path = require('path');
11+
const process = require('process');
12+
13+
const lockfilePath = path.join(__dirname, '..', 'AccessibilityInsightsForAndroidService', 'app', 'gradle.lockfile');
14+
const noticePath = path.join(__dirname, '..', 'NOTICE.html')
15+
16+
// This is to cover cases where we've used code generated by Android Studio (equals/hashCode implementations, for example)
17+
const releaseDepsNotKnownToGradle = [
18+
'The Android Open Source Project'
19+
]
20+
21+
function isDevTarget(target) {
22+
return target === 'lintClassPath' || target === '_internal_aapt2_binary' || /Test/.test(target);
23+
}
24+
function isReleaseTarget(target) {
25+
return !isDevTarget(target);
26+
}
27+
function isEmptyLine(line) {
28+
// whitespace until EOL or start of comment
29+
return /^\s*(\#|$)/.test(line);
30+
}
31+
32+
// input format: lines like "com.android.tools.build:apkzlib:4.0.1=target1,target2"
33+
// output format: ["com.android.tools.build/apkzlib 4.0.1", ...]
34+
function parseReleaseDepsFromLockfile(path) {
35+
const lockfileContent = fs.readFileSync(path).toString();
36+
const lockfileLines = lockfileContent.split(/\r?\n/);
37+
const output = [];
38+
for (const line of lockfileLines) {
39+
if (isEmptyLine(line) || line.startsWith('empty=')) {
40+
continue;
41+
}
42+
43+
const parts = line.split('=');
44+
if (parts.length !== 2) {
45+
throw new Error(`malformatted line: ${line}`);
46+
}
47+
48+
const [dep, targetsLine] = parts;
49+
const targets = targetsLine.split(',');
50+
51+
if (targets.some(isReleaseTarget)) {
52+
const [groupId, artifactId, version] = dep.split(':');
53+
noticeFormatDep = `${groupId}/${artifactId} ${version}`;
54+
output.push(noticeFormatDep);
55+
}
56+
}
57+
output.sort();
58+
return output;
59+
}
60+
61+
// input format: HTML page with snippets per dep like
62+
//
63+
// <summary>
64+
// org.jetbrains/annotations 15.0 - Apache-2.0
65+
// </summary>
66+
//
67+
// output format: ["org.jetbrains/annotatoins 15.0", ...]
68+
function parseDepsFromNoticeFile(path) {
69+
const noticeContent = fs.readFileSync(path).toString();
70+
const componentRegex = /\<summary\>\s*([a-zA-Z0-9\._\- \/]+) - ([a-zA-Z0-9\._\-]+)\s*\<\/summary\>/gm
71+
let captureGroups;
72+
const deps = [];
73+
while ((captureGroups = componentRegex.exec(noticeContent)) !== null) {
74+
deps.push(captureGroups[1]);
75+
}
76+
deps.sort();
77+
return deps;
78+
}
79+
80+
function difference(from, to) {
81+
return from.filter(candidate => !to.includes(candidate));
82+
}
83+
84+
const releaseDeps = parseReleaseDepsFromLockfile(lockfilePath);
85+
releaseDeps.push(...releaseDepsNotKnownToGradle);
86+
87+
const noticeDeps = parseDepsFromNoticeFile(noticePath);
88+
89+
const missingDeps = difference(releaseDeps, noticeDeps);
90+
const extraneousDeps = difference(noticeDeps, releaseDeps);
91+
92+
let exitCode = 0;
93+
94+
if (extraneousDeps.length > 0) {
95+
console.error(`Error: extraneous deps found in ${noticePath} which aren't required by ${lockfilePath}:`);
96+
extraneousDeps.forEach(dep => console.error(` "${dep}"`));
97+
exitCode = 1;
98+
}
99+
if (missingDeps.length > 0) {
100+
console.error(`Error: deps found in ${lockfilePath} but not ${noticePath}:`);
101+
missingDeps.forEach(dep => console.error(` "${dep}"`));
102+
console.error(`If these deps were recently added/updated (eg, this is a Dependabot PR), you will need to regenerate this NOTICE file and commit it to the PR`);
103+
exitCode = 2;
104+
}
105+
106+
process.exit(exitCode);

0 commit comments

Comments
 (0)