fn write_attribution()

in license-scan/src/main.rs [291:413]


fn write_attribution(
    name: &str,
    scan_dir: &Path,
    out_dir: &Path,
    scanner: &ScanStrategy<'_>,
    clarifications: &Clarifications,
    stated_license: Option<Expression>,
) -> Result<()> {
    eprintln!("{}:", name);
    if let Some(stated_license) = stated_license.as_ref() {
        eprintln!("  + {} (stated in metadata)", stated_license);
    }
    let mut files = HashMap::new();
    for entry in WalkBuilder::new(scan_dir).types(TYPES.clone()).build() {
        let entry = entry?;
        if entry.file_type().map_or(false, |ft| ft.is_file()) {
            let rel_path = entry.path().strip_prefix(scan_dir)?;
            let data = fs::read_to_string(entry.path())
                .with_context(|| format!("failed to read {}", entry.path().display()))?;
            let file_hash = hash(&data.as_bytes());
            files.insert(rel_path.to_owned(), (data, file_hash));
        }
    }

    let file_hashes = files
        .iter()
        .map(|(file, (_, hash))| (file.as_path(), *hash))
        .collect();
    let license = if let Some(clarified) = clarifications.get(name, file_hashes)? {
        let expression = clarified.expression.to_string();
        eprintln!("  ! {} (clarified)", expression);
        copy_files(out_dir, &files, clarified.skip_files)?;
        expression
    } else {
        let mut licenses = Vec::new();
        for (file, (data, file_hash)) in &files {
            let containing = scanner
                .scan(&TextData::new(data))
                .map_err(|e| e.compat())?
                .containing;
            if containing.is_empty() {
                if non_license(file) {
                    eprintln!(
                        "  + {} (hash = 0x{:x}) detected as non-license file",
                        file.display(),
                        file_hash
                    );
                } else {
                    if stated_license.is_some() {
                        // if the package states a license and we heuristically detect that this is
                        // a top-level "either license, at your option" file, ignore it
                        let trainwreck = data.split_whitespace().collect::<Vec<_>>().join(" ");
                        if trainwreck.contains("under the terms of either license")
                            || trainwreck.contains("at your option")
                        {
                            eprintln!(
                                "  + {} (hash = 0x{:x}) detected as non-license file",
                                file.display(),
                                file_hash
                            );
                            continue;
                        }
                    }
                    bail!(
                        "failed to detect any license from {} (hash = 0x{:x}), \
                         please add a clarification",
                        scan_dir.join(file).display(),
                        file_hash,
                    );
                }
            }
            for result in containing {
                eprintln!(
                    "  + {} (hash = 0x{:x}) detected as {} (confidence {:.4})",
                    file.display(),
                    file_hash,
                    result.license.name,
                    result.score,
                );
                if let Some(stated_license) = stated_license.as_ref() {
                    // The license we detected should be included in the stated license string,
                    // otherwise we know the stated license is incomplete, in which case we should
                    // have had a clarification.
                    ensure!(
                        stated_license.requirements().any(|er| {
                            // `er` is an `ExpressionReq`; `er.req` is a `LicenseReq`.
                            // `er.req.license.id()` returns `Option<LicenseId>`.
                            er.req.license.id().is_some()
                                && er.req.license.id() == spdx::license_id(result.license.name)
                        }),
                        "detected license \"{}\" from {} is not present in the license \
                         field \"{}\" for {}",
                        result.license.name,
                        file.display(),
                        stated_license,
                        name
                    );
                } else {
                    licenses.push(result.license.name);
                }
            }
        }

        copy_files(out_dir, &files, &[])?;

        if let Some(stated_license) = stated_license {
            stated_license.to_string()
        } else {
            licenses.sort();
            licenses.dedup();
            let expression = licenses.join(" AND ");
            eprintln!("  = {}", expression);
            expression
        }
    };

    fs::create_dir_all(out_dir)?;
    fs::write(
        out_dir.join("attribution.txt"),
        format!("{}\nSPDX-License-Identifier: {}\n", name, license),
    )?;
    Ok(())
}