Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Ensure license headers adher to Cargo manifest #58

Merged
merged 27 commits into from
Sep 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -305,6 +305,16 @@ Can be used to exclude files or directories from the scan.
- Most useful in the combination with `--ensure-licenses`.
- The excluded path can be absolute or relative.

## `--file-extensions` <a name="file-extensions"></a>

Scan only files with the specified extensions.

Examples:

```bash
yarn start -- scan --ensure-licenses Apache-2.0 --file-extensions '.rs' -- /directory/or/file
```

# Implementation <a name="implementation"></a>

[`scan`](https://github.com/paritytech/license-scanner/blob/668b8c5f1cfa1dfc8f22170562f648a344cb60ef/license-scanner/scanner.ts#L141)
Expand Down
2 changes: 2 additions & 0 deletions license-scanner/cli/scan.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ export const executeScan = async function ({
logLevel,
ensureLicenses,
ensureProduct,
fileExtensions,
exclude,
}: ScanCliArgs) {
const licenses = await loadLicensesNormalized(joinPath(buildRoot, "licenses"), {
Expand All @@ -113,6 +114,7 @@ export const executeScan = async function ({
matchLicense,
root: scanRoot,
initialRoot: scanRoot,
fileExtensions,
exclude,
dirs: { crates: cratesDir, repositories: repositoriesDir },
rust: { shouldCheckForCargoLock: true, cargoExecPath: "cargo", rustCrateScannerRoot },
Expand Down
8 changes: 8 additions & 0 deletions license-scanner/license.ts
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,7 @@ export const ensureLicensesInResult = function ({
file,
result,
ensureLicenses,
manifestLicense,
}: EnsureLicensesInResultOptions): Error | undefined {
if (ensureLicenses === false) return;
if (result === undefined) {
Expand All @@ -297,6 +298,13 @@ export const ensureLicensesInResult = function ({
`, expected one of: ${ensureLicenses.join(",")}. Exact file path: "${file.path}"`,
);
}

if (manifestLicense && result.license !== manifestLicense) {
return new Error(
`${file.name} has ${result.license} license` +
`, expected ${manifestLicense} as in cargo manifest. Exact file path: "${file.path}"`,
);
}
};

/**
Expand Down
2 changes: 2 additions & 0 deletions license-scanner/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ program
"If configured, the scan will make sure the product mentioned in the license headers is correct.",
),
)
.option("--file-extensions <fileExtensions...>", "Scan only files with the specified extensions.")
.option("--exclude <exclude...>", "Can be used to exclude files or directories from the scan.")
// It's actually correct usage but @commander-js/extra-typings is wrong on this one.
// eslint-disable-next-line @typescript-eslint/no-misused-promises
Expand All @@ -63,6 +64,7 @@ program
scanRoots: scanRoots.map((scanRoot) => resolvePath(scanRoot)),
startLinesExcludes: await readStartLinesExcludes(options.startLinesExcludes),
detectionOverrides: await readDetectionOverrides(options.detectionOverrides),
fileExtensions: options.fileExtensions ?? [],
exclude: options.exclude ?? [],
logLevel: options.logLevel as LogLevel,
ensureLicenses: readEnsureLicenses(options),
Expand Down
17 changes: 15 additions & 2 deletions license-scanner/scanner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@ const scanCrates = async function (rust: ScanOptionsRust, options: Omit<ScanOpti
export const scan = async function (options: ScanOptions): Promise<ScanResult> {
const {
saveResult,
fileExtensions,
exclude,
root,
rust,
Expand All @@ -143,6 +144,9 @@ export const scan = async function (options: ScanOptions): Promise<ScanResult> {
logger.debug(`Excluding file ${file.path}`);
continue toNextFile;
}
if (fileExtensions.length > 0 && !fileExtensions.some((ext) => file.path.endsWith(ext))) {
mutantcornholio marked this conversation as resolved.
Show resolved Hide resolved
rzadp marked this conversation as resolved.
Show resolved Hide resolved
continue toNextFile;
}
tracker.setFileKey(file.path, key);

logger.debug(`Enqueueing file ${file.path}`);
Expand Down Expand Up @@ -178,7 +182,12 @@ export const scan = async function (options: ScanOptions): Promise<ScanResult> {

await scanQueue.add(async () => {
const result = await matchLicense(file.path);
const licensingError = ensureLicensesInResult({ file, result, ensureLicenses });
const licensingError = ensureLicensesInResult({
file,
result,
ensureLicenses,
manifestLicense: file.manifestLicense,
});
if (licensingError) licensingErrors.push(licensingError);
const productError = ensureProductInFile(file.path, ensureProduct);
if (productError) licensingErrors.push(productError);
Expand All @@ -192,8 +201,12 @@ export const scan = async function (options: ScanOptions): Promise<ScanResult> {

if (rust !== null) {
const rootCargoToml = joinPath(root, "Cargo.toml");
const rootCargoLock = joinPath(root, "Cargo.lock");
if (await existsAsync(rootCargoToml)) {
await scanCrates(rust, options);
await scanCrates(
{ ...rust, shouldCheckForCargoLock: rust.shouldCheckForCargoLock && (await existsAsync(rootCargoLock)) },
options,
);
}
}

Expand Down
3 changes: 3 additions & 0 deletions license-scanner/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ export type ScanOptions = {
saveResult: (projectId: string, filePathFromRoot: string, result: ScanResultItem) => Promise<void>;
root: string;
initialRoot: string;
fileExtensions: string[];
exclude: string[];
dirs: {
repositories: string;
Expand Down Expand Up @@ -101,6 +102,7 @@ export type EnsureLicensesInResultOptions = {
file: { path: string; name: string };
result: ScanResultItem | undefined;
ensureLicenses: boolean | string[];
manifestLicense?: string;
};

export class DatabaseSaveError extends Error {
Expand Down Expand Up @@ -150,6 +152,7 @@ export type CargoMetadataOutputV1 = {

export interface ScanCliArgs {
scanRoots: string[];
fileExtensions: string[];
exclude: string[];
startLinesExcludes: string[];
detectionOverrides: DetectionOverride[];
Expand Down
22 changes: 18 additions & 4 deletions license-scanner/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,25 +16,39 @@ export const lstatAsync = promisify(fs.lstat);
export const writeFileAsync = promisify(fs.writeFile);
const mkdirAsync = promisify(fs.mkdir);

/**
* Recursively traverses down the given directory and yields all found files.
* The files contained within the score of a Cargo.toml manifest are annotated with
* the corresponding license, if it exists.
*/
export const walkFiles: (
dir: string,
options?: {
manifestLicense?: string;
excluding?: { initialRoot: string; exclude: string[] };
},
) => AsyncGenerator<{ path: string; name: string }> = async function* (dir, { excluding } = {}) {
) => AsyncGenerator<{ path: string; name: string; manifestLicense?: string }> = async function* (dir, options = {}) {
const { excluding } = options;
if ((await lstatAsync(dir)).isFile()) {
if (excluding && shouldExclude({ targetPath: dir, ...excluding })) return;
yield { path: dir, name: path.basename(dir) };
yield { path: dir, name: path.basename(dir), manifestLicense: options.manifestLicense };
return;
}

let manifestLicense = options.manifestLicense;
const rootCargoToml = path.join(dir, "Cargo.toml");
if (await existsAsync(rootCargoToml)) {
const manifest = fs.readFileSync(rootCargoToml, "utf-8");
manifestLicense = manifest.match(/license = "(.*)"/)?.[1];
}

for await (const d of await fs.promises.opendir(dir)) {
const fullPath = path.join(dir, d.name);
if (excluding && shouldExclude({ targetPath: fullPath, ...excluding })) continue;
if (d.isDirectory()) {
yield* walkFiles(fullPath, { excluding });
yield* walkFiles(fullPath, { excluding, manifestLicense });
} else {
yield { path: fullPath, name: d.name };
yield { path: fullPath, name: d.name, manifestLicense };
}
}
};
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@paritytech/license-scanner",
"version": "0.0.6",
"version": "0.0.7",
"author": "Parity <admin@parity.io> (https://parity.io)",
"type": "module",
"license": "Apache-2.0",
Expand Down
Loading
Loading