strip.py compiles the stripped tree, so a dual-licensed file that only serves an Enterprise feature fails the import instead of the merge, as v0.16.23's tests/src/directory/issuer.rs does. Upstream's tests of the features the fork rebuilt are expected not to compile there and are listed in build-check-known.txt; an error anywhere else fails the run. Checked against both imports: v0.16.22 passes with its 16 expected errors, v0.16.23 fails on issuer.rs alone. Imports the strip leaves unused are reported. It also renames the upstream name where clients, users or operators meet it as an identifier, from tools/fork/renames.py: wire-protocol names, the web interface's client id, store keys, configuration defaults and the served schema. main is renamed with the same module, so a re-import arrives purged and those lines don't conflict. notice-check.py fails CI when an upstream file the fork changed, measured against the upstream branch, lacks its AGPL 5(a) notice; --fix adds it. It runs beside the name check in a renamed fork-checks job. Also commits v0.16.23's strip report under docs/fork/strip-reports/, which the import in #18 left out.
105 lines
3.9 KiB
Python
Executable File
105 lines
3.9 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
# SPDX-FileCopyrightText: 2026 Coffey Labs
|
|
# SPDX-License-Identifier: AGPL-3.0-only
|
|
"""
|
|
Fail when an upstream file the fork changed doesn't say so (AGPL section 5(a)).
|
|
|
|
tools/fork/notice-check.py # check; exit 1 on a missing notice
|
|
tools/fork/notice-check.py --fix # add the notice where it's missing
|
|
|
|
The AGPL asks a modified work to carry a prominent notice that it was
|
|
modified, with a date. Every upstream file this fork changes carries one
|
|
beneath upstream's own notice:
|
|
|
|
* Modified by Coffey Labs in 2026 for INBUXA.
|
|
|
|
"Changed" is measured against the `upstream` branch, which holds the stripped
|
|
upstream release `main` was last merged with (docs/spec/SPEC.md §2.2a), so
|
|
the list is what actually differs rather than a guess. A file counts as
|
|
upstream's when its header names Stalwart Labs as a copyright holder; files
|
|
the fork wrote carry their own copyright and need nothing. Files with no
|
|
comment header at all (README, manifests) are covered by the README's prose.
|
|
"""
|
|
import argparse
|
|
import datetime
|
|
import re
|
|
import subprocess
|
|
import sys
|
|
|
|
HEADER_LINES = 15
|
|
UPSTREAM_HOLDER = re.compile(r'SPDX-FileCopyrightText:.*Stalwart Labs')
|
|
NOTICE = re.compile(r'Modified by Coffey Labs in \d{4}')
|
|
LICENSE_LINE = re.compile(r'^(\s*(?:\*|//|#)\s*)SPDX-License-Identifier:.*$')
|
|
|
|
|
|
def git(*args):
|
|
return subprocess.run(['git', *args], check=True, capture_output=True, text=True).stdout
|
|
|
|
|
|
def snapshot_ref():
|
|
for ref in ('origin/upstream', 'upstream'):
|
|
if subprocess.run(['git', 'rev-parse', '--verify', '--quiet', f'{ref}^{{commit}}'],
|
|
capture_output=True).returncode == 0:
|
|
return ref
|
|
sys.exit('notice-check: no upstream snapshot branch (origin/upstream or upstream); fetch it first')
|
|
|
|
|
|
def changed_upstream_files(ref):
|
|
# Against the working tree, not HEAD, so it also checks work not yet
|
|
# committed; in CI the two are the same.
|
|
for path in git('diff', '--name-only', '--diff-filter=M', ref).splitlines():
|
|
try:
|
|
head = open(path, encoding='utf-8').read().split('\n')[:HEADER_LINES]
|
|
except (OSError, UnicodeDecodeError):
|
|
continue
|
|
if any(UPSTREAM_HOLDER.search(line) for line in head):
|
|
yield path, head
|
|
|
|
|
|
def add_notice(path):
|
|
"""Put the notice under the license line, in that comment's own style."""
|
|
lines = open(path, encoding='utf-8').read().split('\n')
|
|
for n, line in enumerate(lines[:HEADER_LINES]):
|
|
m = LICENSE_LINE.match(line)
|
|
if m:
|
|
prefix = m.group(1)
|
|
blank = prefix.rstrip()
|
|
year = datetime.date.today().year
|
|
lines[n + 1:n + 1] = [blank, f'{prefix}Modified by Coffey Labs in {year} for INBUXA.']
|
|
open(path, 'w', encoding='utf-8').write('\n'.join(lines))
|
|
return True
|
|
return False
|
|
|
|
|
|
def main():
|
|
ap = argparse.ArgumentParser(description=__doc__.split('\n\n')[0].strip())
|
|
ap.add_argument('--fix', action='store_true', help='add the notice to every file missing it')
|
|
args = ap.parse_args()
|
|
|
|
ref = snapshot_ref()
|
|
checked, missing = 0, []
|
|
for path, head in changed_upstream_files(ref):
|
|
checked += 1
|
|
if not any(NOTICE.search(line) for line in head):
|
|
missing.append(path)
|
|
|
|
if args.fix:
|
|
unfixable = [p for p in missing if not add_notice(p)]
|
|
for p in sorted(set(missing) - set(unfixable)):
|
|
print(f'added: {p}')
|
|
missing = unfixable
|
|
|
|
if missing:
|
|
print(f'{len(missing)} upstream file(s) changed against {ref} without the modification notice:\n')
|
|
for p in missing:
|
|
print(f' {p}')
|
|
print('\nAdd "Modified by Coffey Labs in <year> for INBUXA." under the license line, '
|
|
'or run tools/fork/notice-check.py --fix.')
|
|
return 1
|
|
print(f'notice check: clean ({checked} changed upstream file(s), all marked; against {ref}).')
|
|
return 0
|
|
|
|
|
|
if __name__ == '__main__':
|
|
sys.exit(main())
|