JJ_MKVMaker/tool/licenses/rhwp_crates.py

141 lines
7.3 KiB
Python

"""관리자 지시 (10/20, 28 지적): libjj_rhwp.so 에 실제로 링크되는 Rust crate 마다 이름 · 판 · 라이선스와 원문 · 저작권 고지를 모아
assets/licenses/RHWP_LICENSES.txt 를 만든다 (tool/build_rhwp_android.sh 가 빌드 뒤에 부른다).
- 링크되는 것: cargo tree --target aarch64-linux-android -e normal,no-proc-macro (빌드 전용 · 개발 · proc-macro 는 .so 에 들어가지 않음)
- 원문: crate 폴더 맨 위의 LICENSE* · LICENCE* · COPYING* · NOTICE* · COPYRIGHT* · UNLICENSE* (cargo metadata 의 manifest_path)
원문 파일이 없는 crate 는 같은 라이선스의 원문을 맨 뒤 "라이선스 원문" 에 한 번씩 넣는다.
- Cargo.lock 의 나머지는 "링크되지 않음" 목록에 적는다 - test/licenses_test.dart 가 두 목록의 합이 Cargo.lock 과 같은지 본다
(Cargo.lock 이 바뀌었는데 이 파일을 다시 만들지 않으면 시험이 잡는다).
- rhwp 저장소의 THIRD_PARTY_LICENSES.md (npm 쪽 포함) 는 쓰지 않는다.
사용: python tool/licenses/rhwp_crates.py <ffi Cargo.toml> <rhwp 소스 폴더> <rhwp 판> <rhwp 커밋> <출력 파일>
(cargo 는 PATH · CARGO_HOME 의 것, 의존은 이미 받아 둔 것 - --offline --locked)
"""
import hashlib
import json
import os
import re
import subprocess
import sys
manifest, rhwp_src, version, commit, out = sys.argv[1:6]
TARGET = 'aarch64-linux-android'
def cargo(*args):
r = subprocess.run(['cargo', *args, '--manifest-path', manifest, '--locked', '--offline'],
capture_output=True, text=True, encoding='utf-8')
if r.returncode != 0:
sys.exit('cargo %s 실패:\n%s' % (args[0], r.stderr))
return r.stdout
# 1. 링크되는 crate (이름, 판)
linked = set()
for line in cargo('tree', '--target', TARGET, '-e', 'normal,no-proc-macro', '--prefix', 'none', '--format', '{p}').splitlines():
m = re.match(r'^(\S+) v(\S+)', line.strip())
if m:
linked.add((m.group(1), m.group(2)))
# 2. 모든 crate 의 폴더 · 라이선스
meta = json.loads(cargo('metadata', '--format-version', '1', '--filter-platform', TARGET))
pkgs = {(p['name'], p['version']): p for p in meta['packages']}
# 3. Cargo.lock 의 모든 crate
lock = open(os.path.join(os.path.dirname(manifest), 'Cargo.lock'), encoding='utf-8').read()
locked = set(re.findall(r'\[\[package\]\]\s*\nname = "([^"]+)"\s*\nversion = "([^"]+)"', lock))
missing = linked - locked
if missing:
sys.exit('Cargo.lock 에 없는 crate: %s' % sorted(missing))
ours = {'jj_rhwp'}
NAMES = re.compile(r'^(LICEN[CS]E|COPYING|NOTICE|COPYRIGHT|UNLICENSE)', re.I)
def license_files(pkg):
d = os.path.dirname(pkg['manifest_path'])
files = []
for n in sorted(os.listdir(d)):
p = os.path.join(d, n)
if os.path.isfile(p) and NAMES.match(n):
files.append((n, open(p, encoding='utf-8', errors='replace').read().strip()))
if pkg.get('license_file'):
p = os.path.join(d, pkg['license_file'])
if os.path.isfile(p) and os.path.basename(p) not in [f for f, _ in files]:
files.append((os.path.basename(p), open(p, encoding='utf-8', errors='replace').read().strip()))
return files
# 원문이 없는 crate 를 위해 라이선스 이름 → 원문 하나 (같은 이름의 원문 파일 하나만 가진 crate 에서)
SPDX_FILE = {'MIT': r'LICEN[CS]E-?MIT', 'Apache-2.0': r'LICEN[CS]E-?APACHE', 'BSD-3-Clause': r'LICEN[CS]E-?BSD',
'Zlib': r'LICEN[CS]E-?ZLIB', 'BSL-1.0': r'LICEN[CS]E-?BOOST', 'Unicode-3.0': r'LICEN[CS]E-?UNICODE'}
spdx_text = {}
sections = []
seen_text = {}
no_file = []
for key in sorted(linked, key=lambda k: (k[0].lower(), k[1])):
name, ver = key
pkg = pkgs[key]
lic = pkg.get('license') or ('see ' + pkg['license_file'] if pkg.get('license_file') else '?')
if name in ours:
continue
files = license_files(pkg)
for fname, text in files:
for spdx, pat in SPDX_FILE.items():
if re.fullmatch(pat + r'(\.(txt|md))?', fname, re.I) and spdx not in spdx_text:
spdx_text[spdx] = (text, '%s %s' % (name, ver))
body = []
for fname, text in files:
h = hashlib.sha256(text.encode()).hexdigest()
if h in seen_text:
body.append('[%s] (%s 의 %s 와 같은 글)' % (fname, *seen_text[h]))
else:
seen_text[h] = ('%s %s' % (name, ver), fname)
body.append('[%s]\n%s' % (fname, text))
if not files:
no_file.append((name, ver, lic))
body.append('(crate 에 원문 파일이 없음 - 아래 "라이선스 원문" 의 %s)' % lic)
authors = ', '.join(pkg.get('authors') or [])
sections.append('=== %s %s (%s) ===\n%s%s\n%s' % (
name, ver, lic, 'Authors: %s\n' % authors if authors else '', pkg.get('repository') or '', '\n\n'.join(body)))
not_linked = sorted(locked - linked, key=lambda k: (k[0].lower(), k[1]))
lines = [
'rhwp v%s (https://github.com/edwardkim/rhwp, commit %s) - MIT' % (version, commit),
'Android: libjj_rhwp.so (HWP / HWPX -> PDF), built by tool/build_rhwp_android.sh with tool/rhwp_ffi (MIT)',
'This file lists every Rust crate linked into libjj_rhwp.so (target %s, normal dependencies,' % TARGET,
'build-only / dev / proc-macro crates excluded) with its license and the license / notice files it ships.',
'',
'---------------------------------------------------------------------',
'rhwp LICENSE',
'---------------------------------------------------------------------',
open(os.path.join(rhwp_src, 'LICENSE'), encoding='utf-8').read().strip(),
'',
'---------------------------------------------------------------------',
'Linked crates (%d)' % len(linked - {(n, v) for n, v in linked if n in ours}),
'---------------------------------------------------------------------',
]
for name, ver in sorted(linked, key=lambda k: (k[0].lower(), k[1])):
if name not in ours:
lines.append('linked: %s %s (%s)' % (name, ver, pkgs[(name, ver)].get('license') or '?'))
lines += ['', '---------------------------------------------------------------------',
'Not linked into libjj_rhwp.so (build tools, proc-macros, other platforms) - listed for completeness',
'---------------------------------------------------------------------']
lines += ['not linked: %s %s' % k for k in not_linked]
lines += ['', '---------------------------------------------------------------------', 'License and notice files',
'---------------------------------------------------------------------', '']
lines += [s + '\n' for s in sections]
need = sorted({l for _, _, l in no_file})
if need:
lines += ['---------------------------------------------------------------------', '라이선스 원문 (원문 파일이 없는 crate 용)',
'---------------------------------------------------------------------', '']
for lic in need:
for spdx in re.findall(r'[A-Za-z0-9.\-]+', lic):
if spdx in spdx_text:
text, src = spdx_text.pop(spdx)
lines += ['=== %s (원문: %s 의 파일) ===' % (spdx, src), text, '']
text = '\n'.join(lines).rstrip() + '\n'
open(out, 'wb').write(text.replace('\n', '\r\n').encode('utf-8'))
print('linked %d · not linked %d · 원문 파일 없음 %d %s' % (len(linked) - 1, len(not_linked), len(no_file),
[f'{n} {v}' for n, v, _ in no_file]))