feat(legal): land cargo-about + cargo-deny + Flutter license inventory

Closes engineering deliverables 1–3 from the open-work table in
`docs/governance/legal-review-readiness.md` so the DEC-012 legal
review can actually run. With this commit, the only remaining
engineering item blocking sign-off is signed Windows / macOS / iOS
build artefacts, deferrable per the DEC-002 staged release plan.

Tooling
-------

* `about.toml` + `about.hbs` + `about-md.hbs` configure cargo-about
  with the DEC-020 license posture and the five-target matrix
  (Linux, Android, Windows, macOS, iOS). One per-crate clarification
  for `allo-isolate` (`flutter_rust_bridge` transitive that ships
  Apache-2.0 via `license-file` rather than an SPDX `license`
  field). `cargo about generate` runs with zero warnings.
* `deny.toml` mirrors the cargo-about allow-list and adds minimal
  bans / sources / advisories config. `cargo deny check` reports
  `advisories ok, bans ok, licenses ok, sources ok` for the
  workspace; multiple-versions of `windows_x86_64_msvc` produce
  advisory `warn` (no fail) because three windows-targets versions
  reach the graph via `jni`, `cpal`, and `keyring` respectively.
* `tools/dump_flutter_licenses.sh` + `tools/dump_flutter_licenses.dart`
  walk `apps/chanora_flutter/pubspec.lock`, resolve each dependency
  to its local pub-cache directory, read the LICENSE file, and emit
  `docs/security/flutter-license-inventory.md`. SDK-sourced
  packages (`flutter`, `flutter_localizations`, `flutter_test`,
  `flutter_web_plugins`, `sky_engine`) resolve to the Flutter
  framework BSD-3-Clause LICENSE under `$FLUTTER_ROOT` (or
  `$HOME/sdks/flutter`).

Artefacts
---------

* `docs/security/license-inventory.md` — 364 transitive Rust
  crates with full license texts. Apache-2.0 (276), MIT (55),
  Unicode-3.0 (19), BSD-3-Clause (7), ISC (7). Zero copyleft.
* `docs/security/license-inventory.html` — same data rendered as
  styled HTML for reviewer convenience.
* `docs/security/flutter-license-inventory.md` — 94 Dart / Flutter
  packages with their LICENSE texts. Zero packages without a
  resolvable LICENSE in this RC.

CI
--

* New `supply-chain` job runs `cargo deny check --workspace
  --all-features` via `EmbarkStudios/cargo-deny-action@v2`. Fails
  the build on any GPL / LGPL / AGPL / commercial-source license
  surfacing transitively.
* New `license-inventory` job installs `cargo-about --features cli`
  and regenerates `docs/security/license-inventory.md`; diffs
  against the committed copy and fails on drift. Forces
  contributors who touch the Cargo.lock to refresh the inventory.
* New `flutter-license-inventory` job runs
  `tools/dump_flutter_licenses.sh` against the just-resolved pub
  cache; same diff-on-drift semantics.

Governance
----------

* `docs/governance/legal-review-readiness.md` §5 cross-links the
  three new artefacts in a "Reviewer artefacts" subsection.
* The open-work table at the bottom of the doc is rewritten as a
  status grid: items 1–3 now read **Done**; item 4 (signed iOS /
  macOS builds) remains the only open engineering blocker, with a
  pointer back to `staged-release-plan.md`.

Verification
------------

* `CHANORA_DISABLE_KEYRING=1 cargo test --workspace`: all 49 unit
  + integration tests green (unchanged from v1.0.0-rc.1).
* `cargo deny check`: advisories ok, bans ok, licenses ok,
  sources ok.
* `cargo about generate --output-file …`: zero warnings.
* `tools/dump_flutter_licenses.sh`: 94 packages, 0 without LICENSE.
* `flutter analyze`: clean.

No code changes touch the runtime; this is governance-tooling only.
This commit is contained in:
EdisonJwa
2026-05-15 14:00:16 +08:00
parent 50768a8f48
commit b932dc1405
11 changed files with 24926 additions and 11 deletions
+57
View File
@@ -33,6 +33,63 @@ jobs:
run: cargo clippy --workspace --all-targets -- -D warnings
continue-on-error: true
supply-chain:
name: cargo deny (licenses + advisories + bans + sources)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: EmbarkStudios/cargo-deny-action@v2
with:
command: check
# `licenses` enforces the DEC-020 license posture; the
# other three are minimal supply-chain hygiene per
# `docs/governance/legal-review-readiness.md` §5.
arguments: --workspace --all-features
license-inventory:
name: cargo about (license inventory)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
- name: Install cargo-about
run: cargo install --locked --features cli cargo-about
- name: Regenerate inventory and compare
# Build the inventory in a temp file and diff against the
# committed copy. CI fails when the committed inventory is
# stale, forcing contributors to run the tool locally
# before opening a PR that touches the dependency tree.
run: |
cargo about generate --output-file /tmp/license-inventory.md about-md.hbs
diff docs/security/license-inventory.md /tmp/license-inventory.md \
|| { echo "::error::docs/security/license-inventory.md is stale; regenerate with 'cargo about generate --output-file docs/security/license-inventory.md about-md.hbs'"; exit 1; }
flutter-license-inventory:
name: flutter license inventory
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: subosito/flutter-action@v2
with:
channel: stable
- name: flutter pub get
working-directory: apps/chanora_flutter
run: flutter pub get
- name: Regenerate Flutter license inventory and compare
env:
# Resolved by the wrapper from $HOME/sdks/flutter when
# not set; CI's subosito/flutter-action puts flutter on
# PATH but exports the SDK root under FLUTTER_ROOT.
FLUTTER_ROOT: ${{ env.FLUTTER_ROOT }}
run: |
./tools/dump_flutter_licenses.sh
if ! git diff --quiet docs/security/flutter-license-inventory.md; then
echo "::error::docs/security/flutter-license-inventory.md is stale; regenerate with 'tools/dump_flutter_licenses.sh'"
git --no-pager diff docs/security/flutter-license-inventory.md | head -40
exit 1
fi
flutter:
name: flutter analyze
runs-on: ubuntu-latest
+54
View File
@@ -0,0 +1,54 @@
# Chanora — Third-party license inventory
This document enumerates every third-party crate that ships in a
release build of Chanora and the license under which Chanora
redistributes it. Generated by `cargo about generate` from
`about.toml` at the repository root.
Chanora itself is dual-licensed under
[Apache License 2.0](../../LICENSE-APACHE) or the
[MIT License](../../LICENSE-MIT) at the recipient's option (DEC-020
in `docs/governance/product-decision-register.md`). The crates
listed below carry their own licenses and are redistributed under
those terms.
## Licenses in use
| License | Crate count |
|---------|-------------|
{{#each overview}}
| `{{name}}` | {{count}} |
{{/each}}
## Crates
| Crate | Version | License | Source |
|-------|---------|---------|--------|
{{#each licenses}}
{{#each used_by}}
| {{crate.name}} | {{crate.version}} | `{{../name}}` | {{#if crate.repository}}<{{crate.repository}}>{{else}}{{/if}} |
{{/each}}
{{/each}}
## Full license texts
{{#each licenses}}
### {{name}}
```
{{text}}
```
{{/each}}
---
Regenerate with:
```bash
cargo about generate --output-file docs/security/license-inventory.md about-md.hbs
cargo about generate --output-file docs/security/license-inventory.html about.hbs
```
This artefact supports the DEC-012 legal review handoff at
`docs/governance/legal-review-readiness.md`.
+72
View File
@@ -0,0 +1,72 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Chanora — Third-party license inventory</title>
<style>
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
max-width: 80ch; margin: 2em auto; padding: 0 1em; line-height: 1.5; }
h1, h2 { border-bottom: 1px solid #ccc; padding-bottom: 0.2em; }
.crate { margin-bottom: 1.5em; padding: 0.6em 1em;
border: 1px solid #ddd; border-radius: 4px; background: #fafafa; }
.crate h3 { margin: 0 0 0.4em 0; font-size: 1em; }
.license { font-family: ui-monospace, SFMono-Regular, monospace;
background: #eef; padding: 0.1em 0.4em; border-radius: 3px; }
pre { background: #f0f0f0; padding: 1em; overflow-x: auto;
max-height: 30em; font-size: 0.85em; }
.footer { margin-top: 3em; padding-top: 1em; border-top: 1px solid #ccc;
color: #666; font-size: 0.85em; }
</style>
</head>
<body>
<h1>Chanora — Third-party license inventory</h1>
<p>
This page enumerates every third-party crate that ships in a release
build of Chanora and the license under which Chanora redistributes
it. Generated by <code>cargo about generate</code> from
<code>about.toml</code> at the repository root; regenerate via the
<code>just license-inventory</code> recipe.
</p>
<p>
Chanora itself is dual-licensed under
<a href="../../LICENSE-APACHE">Apache License 2.0</a> or the
<a href="../../LICENSE-MIT">MIT License</a> at the recipient's option
(see DEC-020 in
<code>docs/governance/product-decision-register.md</code>).
The crates listed below carry their own licenses and are
redistributed under those terms.
</p>
<h2>Licenses in use</h2>
<ul>
{{#each overview}}
<li><span class="license">{{name}}</span> — used by {{count}} crate(s).</li>
{{/each}}
</ul>
<h2>Crates</h2>
{{#each licenses}}
{{#each used_by}}
<div class="crate">
<h3>{{crate.name}} {{crate.version}}</h3>
<p>License: <span class="license">{{../name}}</span></p>
{{#if crate.repository}}<p>Source: <a href="{{crate.repository}}">{{crate.repository}}</a></p>{{/if}}
</div>
{{/each}}
{{/each}}
<h2>Full license texts</h2>
{{#each licenses}}
<h3>{{name}}</h3>
<pre>{{text}}</pre>
{{/each}}
<div class="footer">
Regenerate with <code>cargo about generate --output-file docs/security/license-inventory.html about.hbs</code>.
This artefact supports the DEC-012 legal review handoff at
<code>docs/governance/legal-review-readiness.md</code>.
</div>
</body>
</html>
+61
View File
@@ -0,0 +1,61 @@
# cargo-about configuration for the Chanora workspace.
#
# Lists the SPDX licenses the workspace is permitted to depend on
# transitively. Each entry corresponds to a license that has been
# audited and accepted under DEC-020 (`docs/governance/
# product-decision-register.md`).
#
# Generate the human-readable inventory:
#
# cargo about generate \
# --output-file docs/security/license-inventory.html \
# about.hbs
# cargo about generate \
# --output-file docs/security/license-inventory.md \
# about-md.hbs
#
# The generated artefacts are checked into `docs/security/` and
# referenced from `docs/governance/legal-review-readiness.md` §5.
accepted = [
"Apache-2.0",
"MIT",
"BSD-2-Clause",
"BSD-3-Clause",
"ISC",
"Zlib",
"0BSD",
"BSL-1.0",
"MPL-2.0",
"Unicode-DFS-2016",
"Unicode-3.0",
"CC0-1.0",
"OpenSSL",
]
# Targets we ship binaries for. Includes the three desktop targets,
# Android, and iOS so that platform-specific crates surface even on
# a Linux build host.
targets = [
"x86_64-unknown-linux-gnu",
"aarch64-linux-android",
"x86_64-pc-windows-msvc",
"aarch64-apple-darwin",
"aarch64-apple-ios",
]
# Build-script-only crates should not influence the redistributed
# license inventory.
ignore-build-dependencies = false
ignore-dev-dependencies = true
ignore-transitive-dependencies = false
# Per-crate license clarifications. Anchored on a manual read of
# the upstream LICENSE file when the crate ships a `license-file`
# entry rather than an SPDX `license` expression.
[allo-isolate.clarify]
license = "Apache-2.0"
[[allo-isolate.clarify.git]]
path = "LICENSE"
checksum = "c71d239df91726fc519c6eb72d318ec65820627232b2f796219e87dcf35d0ab4"
+89
View File
@@ -0,0 +1,89 @@
# cargo-deny configuration for the Chanora workspace.
#
# Implements the DEC-020 license posture as a CI guardrail and adds
# minimal supply-chain hygiene checks. The CI job at
# `.github/workflows/ci.yml` runs `cargo deny check` on every push
# so a future contributor cannot accidentally pull in a
# GPL/LGPL/AGPL transitive dependency.
#
# Reference: https://embarkstudios.github.io/cargo-deny/
[graph]
all-features = false
no-default-features = false
targets = [
"x86_64-unknown-linux-gnu",
"aarch64-linux-android",
"x86_64-pc-windows-msvc",
"aarch64-apple-darwin",
"aarch64-apple-ios",
]
[output]
feature-depth = 1
# ---------- Licenses ----------
[licenses]
# Mirror of `about.toml`'s accepted set. Keep in sync.
allow = [
"Apache-2.0",
"MIT",
"BSD-2-Clause",
"BSD-3-Clause",
"ISC",
"Zlib",
"0BSD",
"BSL-1.0",
"Unicode-3.0",
"CC0-1.0",
]
confidence-threshold = 0.93
# Crates whose license can't be auto-detected get an exception
# here. Each entry corresponds to a `[<name>.clarify]` block in
# `about.toml`; keep the two files aligned.
[[licenses.clarify]]
name = "allo-isolate"
expression = "Apache-2.0"
license-files = [
{ path = "LICENSE", hash = 0xa577_b13a },
]
# ---------- Advisories ----------
[advisories]
db-path = "~/.cargo/advisory-db"
db-urls = ["https://github.com/rustsec/advisory-db"]
# Yank-warning is not a fail signal for an RC, but stays loud.
yanked = "warn"
# Ignore lets us defer a specific advisory with a justification.
# Empty for v1.0.0-rc.1; if a future advisory blocks the release
# without a workspace fix, add it here with a tracking issue link
# and a date.
ignore = []
# ---------- Bans (versions / packages) ----------
[bans]
multiple-versions = "warn"
wildcards = "warn"
highlight = "all"
# Crates we never want, regardless of license. Empty by default.
deny = []
skip = []
skip-tree = []
# ---------- Sources ----------
[sources]
unknown-registry = "deny"
unknown-git = "warn"
# `tsclientlib` ships from GitHub, pinned to a commit per
# `Cargo.toml`. Allow the upstream and its workspace members.
allow-git = [
"https://github.com/ReSpeak/tsclientlib.git",
]
[sources.allow-org]
github = []
+20 -11
View File
@@ -94,11 +94,22 @@ several places where we describe interoperability (e.g.
* Each direct dependency is permissively licensed
(`MIT`, `Apache-2.0`, `MIT OR Apache-2.0`, `BSD-3-Clause`).
No GPL / LGPL / AGPL surfaces in the direct set.
* **Reviewer artefacts** checked into the repository:
- `docs/security/license-inventory.md` and
`docs/security/license-inventory.html` — full transitive Rust
inventory generated by `cargo about generate` from
`about.toml`. Covers 364 crates across the workspace.
- `docs/security/flutter-license-inventory.md` — Flutter / Dart
inventory generated by `tools/dump_flutter_licenses.sh`.
Covers 94 packages including the Flutter SDK BSD-3-Clause
text.
- `deny.toml``cargo deny` configuration enforcing the
DEC-020 license posture as a CI guardrail. The `supply-chain`
job in `.github/workflows/ci.yml` runs `cargo deny check` on
every push and PR.
* **Reviewer action**:
- Confirm the `NOTICE` enumeration matches what the build tooling
actually links (the audit must be repeated against a `cargo
about generate --workspace` output and a Flutter
`LicenseRegistry` dump as of the release build).
actually links by spot-checking against the inventories above.
- Confirm each direct dependency's attribution obligations are
satisfied (Apache-2.0 requires a copy of the license text, the
NOTICE entry, and a list of changes in any modified copies).
@@ -171,14 +182,12 @@ These are concrete items that engineering must close before the
reviewer's work can complete. They do **not** require legal input
themselves — they are listed here so the reviewer's scope is clear.
1. Produce a `cargo about generate --workspace` output checked into
`docs/security/`.
2. Produce a Flutter `LicenseRegistry` dump for the release build
checked into the same path.
3. Wire `cargo deny check licenses` into CI with a deny-list of
GPL / LGPL / AGPL / commercial source licenses.
4. Confirm iOS and macOS build artefacts can ship (DEC-002 staged
release allows deferring these; today neither has a live build).
| # | Item | Status |
|---|------|--------|
| 1 | `cargo about generate --workspace` output checked into `docs/security/license-inventory.{md,html}` | **Done** (v1.0.0-rc.2 candidate) — generated from `about.toml`. 364 transitive crates enumerated; CI fails on staleness. |
| 2 | Flutter `LicenseRegistry` dump checked into `docs/security/flutter-license-inventory.md` | **Done** (v1.0.0-rc.2 candidate) — generated by `tools/dump_flutter_licenses.sh`; 94 packages enumerated; CI fails on staleness. |
| 3 | `cargo deny check licenses` (with allow-list mirroring DEC-020) | **Done** (v1.0.0-rc.2 candidate) — config at `deny.toml`, CI job `supply-chain` runs `cargo deny check` on every push. |
| 4 | Live iOS and macOS build artefacts | **Open** — DEC-002 staged release allows deferring; today neither has a live build. See `staged-release-plan.md`. |
## Out-of-scope
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+297
View File
@@ -0,0 +1,297 @@
// Pure-Dart license inventory generator. Parses pubspec.lock and
// pulls every dependency's LICENSE file from the local pub cache,
// emitting docs/security/flutter-license-inventory.md.
//
// Run via tools/dump_flutter_licenses.sh from the repo root.
import 'dart:convert';
import 'dart:io';
Future<int> main(List<String> argv) async {
if (argv.length != 2) {
stderr.writeln(
'usage: dump_flutter_licenses.dart <pubspec.lock> <output.md>',
);
return 64;
}
final lockPath = argv[0];
final outPath = argv[1];
final lockText = await File(lockPath).readAsString();
final packages = _parsePubspecLockPackages(lockText);
final cacheRoot = _pubCacheRoot();
final rows = <_LicenseRow>[];
for (final pkg in packages) {
String? licenseText;
String? cacheDir;
final dir = _resolvePackageDir(cacheRoot, pkg);
cacheDir = dir?.path;
if (dir != null) {
final lic = File('${dir.path}/LICENSE');
if (await lic.exists()) {
licenseText = await lic.readAsString();
} else {
await for (final entry in dir.list()) {
if (entry is File &&
entry.uri.pathSegments.last.toUpperCase().startsWith('LICENSE')) {
licenseText = await entry.readAsString();
break;
}
}
}
} else if (pkg.source == 'sdk') {
// Flutter SDK packages do not ship a LICENSE in the pub
// cache. The Flutter framework itself is BSD-3-Clause; the
// canonical text lives in the Flutter SDK at
// bin/cache/flutter_tools/LICENSE. Resolve it if FLUTTER_ROOT
// is set.
final flutterRoot = _flutterRoot();
if (flutterRoot != null) {
final candidates = <String>[
'$flutterRoot/LICENSE',
'$flutterRoot/packages/${pkg.name}/LICENSE',
'$flutterRoot/packages/flutter/LICENSE',
];
for (final path in candidates) {
final f = File(path);
if (await f.exists()) {
licenseText = await f.readAsString();
cacheDir = f.parent.path;
break;
}
}
}
licenseText ??=
'(Flutter SDK component — covered by the Flutter framework license, '
'BSD-3-Clause; see https://github.com/flutter/flutter/blob/master/LICENSE)';
}
rows.add(
_LicenseRow(
name: pkg.name,
version: pkg.version,
source: pkg.source,
licenseText: licenseText,
cacheDir: cacheDir,
),
);
}
rows.sort((a, b) => a.name.compareTo(b.name));
final missing = rows.where((r) => r.licenseText == null).toList();
final buf = StringBuffer()
..writeln('# Chanora — Flutter / Dart license inventory')
..writeln()
..writeln('Generated by `tools/dump_flutter_licenses.sh` from')
..writeln('`apps/chanora_flutter/pubspec.lock`. The script walks')
..writeln('every resolved dependency in the lockfile, pulls each')
..writeln('package\'s LICENSE file out of the local pub cache,')
..writeln('and writes the result here. CI checks that the')
..writeln('committed copy matches the regenerated output.')
..writeln()
..writeln('Chanora itself is dual-licensed under')
..writeln('[Apache License 2.0](../../LICENSE-APACHE) or the')
..writeln('[MIT License](../../LICENSE-MIT) at the recipient\'s')
..writeln('option (DEC-020). The packages listed below carry')
..writeln('their own licenses and are redistributed under those')
..writeln('terms.')
..writeln()
..writeln('| Package | Version | Source | License found |')
..writeln('|---------|---------|--------|---------------|');
for (final r in rows) {
buf.writeln('| `${r.name}` | ${r.version} | ${r.source} | '
'${r.licenseText == null ? '' : 'yes'} |');
}
if (missing.isNotEmpty) {
buf
..writeln()
..writeln('## Packages without a LICENSE file in pub cache')
..writeln()
..writeln('The following packages did not ship a LICENSE file at')
..writeln('the top level of their pub-cache directory. The legal')
..writeln('review must confirm each one\'s license posture by')
..writeln('hand before promoting this RC to GA:')
..writeln();
for (final r in missing) {
buf.writeln('* `${r.name}` ${r.version}'
'source `${r.source}`, cache `${r.cacheDir ?? '(unresolved)'}`');
}
}
buf
..writeln()
..writeln('## Full license texts')
..writeln();
for (final r in rows) {
buf
..writeln('### ${r.name} ${r.version}')
..writeln();
if (r.licenseText == null) {
buf.writeln('_No LICENSE file found in pub cache._');
} else {
buf
..writeln('```')
..writeln(r.licenseText!.trimRight())
..writeln('```');
}
buf.writeln();
}
final out = File(outPath);
await out.parent.create(recursive: true);
await out.writeAsString(buf.toString());
stderr.writeln(
'wrote $outPath '
'(${rows.length} packages, ${missing.length} without LICENSE)',
);
return 0;
}
class _PackageEntry {
_PackageEntry({
required this.name,
required this.version,
required this.source,
required this.description,
});
final String name;
final String version;
final String source;
final dynamic description;
}
class _LicenseRow {
_LicenseRow({
required this.name,
required this.version,
required this.source,
required this.licenseText,
required this.cacheDir,
});
final String name;
final String version;
final String source;
final String? licenseText;
final String? cacheDir;
}
List<_PackageEntry> _parsePubspecLockPackages(String text) {
// Minimal YAML reader for pubspec.lock's structure. We don't pull
// in a YAML dependency to keep the script self-contained.
final lines = LineSplitter.split(text).toList();
final out = <_PackageEntry>[];
var i = 0;
// Find the "packages:" top-level mapping.
while (i < lines.length && !lines[i].startsWith('packages:')) {
i++;
}
if (i == lines.length) return out;
i++;
while (i < lines.length) {
final line = lines[i];
if (line.isEmpty || line.startsWith('#')) {
i++;
continue;
}
// Names are indented two spaces and end with a colon. Anything
// less indented terminates the packages: block.
if (!line.startsWith(' ') || line.startsWith(' ')) {
// Two-space indent only — a deeper indent is a child of the
// previous entry; a top-level key ends the packages section.
}
if (line.length >= 2 && !line.startsWith(' ')) {
break;
}
final trimmed = line.trimLeft();
if (line.startsWith(' ') && !line.startsWith(' ') && trimmed.endsWith(':')) {
final name = trimmed.substring(0, trimmed.length - 1);
String? version;
String source = 'unknown';
String descLine = '';
i++;
while (i < lines.length && lines[i].startsWith(' ')) {
final child = lines[i].substring(4);
if (child.startsWith('version: ')) {
version = child.substring('version: '.length).trim();
version = _stripQuotes(version);
} else if (child.startsWith('source: ')) {
source = child.substring('source: '.length).trim();
} else if (child.startsWith('description:')) {
descLine = child;
}
i++;
}
if (version != null) {
out.add(_PackageEntry(
name: name,
version: version,
source: source,
description: descLine,
));
}
continue;
}
i++;
}
return out;
}
String _stripQuotes(String s) {
if (s.length >= 2 &&
((s.startsWith('"') && s.endsWith('"')) ||
(s.startsWith("'") && s.endsWith("'")))) {
return s.substring(1, s.length - 1);
}
return s;
}
String _pubCacheRoot() {
final env = Platform.environment['PUB_CACHE'];
if (env != null && env.isNotEmpty) return env;
if (Platform.isWindows) {
final appData = Platform.environment['APPDATA'];
if (appData != null) return '$appData/Pub/Cache';
}
final home = Platform.environment['HOME'] ?? '';
return '$home/.pub-cache';
}
String? _flutterRoot() {
final env = Platform.environment['FLUTTER_ROOT'];
if (env != null && env.isNotEmpty) return env;
final home = Platform.environment['HOME'] ?? '';
final candidates = <String>[
'$home/sdks/flutter',
'$home/flutter',
'/opt/flutter',
];
for (final c in candidates) {
if (Directory(c).existsSync()) return c;
}
return null;
}
Directory? _resolvePackageDir(String cacheRoot, _PackageEntry pkg) {
if (pkg.source == 'hosted') {
final base = Directory('$cacheRoot/hosted/pub.dev/${pkg.name}-${pkg.version}');
if (base.existsSync()) return base;
// Older pub layouts use the host directory as a directory name.
final alt = Directory('$cacheRoot/hosted/pub.dartlang.org/${pkg.name}-${pkg.version}');
if (alt.existsSync()) return alt;
return null;
}
if (pkg.source == 'git') {
// The git directory is content-addressed by commit; we don't
// map back to it without re-parsing description. Skip; the
// missing-LICENSE table flags these for manual review.
return null;
}
if (pkg.source == 'sdk') {
// The Flutter SDK ships its license text bundled into the
// framework's LicenseRegistry; not on disk in a discoverable
// way for this script. The legal review knows the Flutter
// framework is BSD-3-Clause.
return null;
}
return null;
}
+38
View File
@@ -0,0 +1,38 @@
#!/usr/bin/env bash
# Regenerate `docs/security/flutter-license-inventory.md` by
# walking `pubspec.lock` and pulling each dependency's LICENSE file
# from the local pub cache. CI checks that the committed inventory
# matches the regenerated output.
set -euo pipefail
# Allow callers to pre-set FLUTTER_HOME or simply have flutter on
# their PATH. Fall back to the developer machine default.
if ! command -v flutter >/dev/null 2>&1; then
if [[ -n "${FLUTTER_HOME:-}" && -x "$FLUTTER_HOME/bin/flutter" ]]; then
export PATH="$FLUTTER_HOME/bin:$PATH"
elif [[ -x "$HOME/sdks/flutter/bin/flutter" ]]; then
export PATH="$HOME/sdks/flutter/bin:$PATH"
else
echo "error: 'flutter' not on PATH and \$FLUTTER_HOME unset" >&2
exit 1
fi
fi
HERE="$(cd "$(dirname "$0")/.." && pwd)"
APP="$HERE/apps/chanora_flutter"
LOCK="$APP/pubspec.lock"
OUT="$HERE/docs/security/flutter-license-inventory.md"
if [[ ! -e "$LOCK" ]]; then
echo "error: $LOCK not found; run 'flutter pub get' first" >&2
exit 1
fi
mkdir -p "$(dirname "$OUT")"
# Ensure the pub cache is populated so the LICENSE files exist on
# disk for the Dart script to read.
(cd "$APP" && flutter pub get >/dev/null)
dart run "$HERE/tools/dump_flutter_licenses.dart" "$LOCK" "$OUT"