141 lines
7.3 KiB
Python
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]))
|