Files

142 lines
7.7 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
# //! 安装文件清单与校验;不管理桌面服务、系统文件或用户数据。
import hashlib
import json
import os
from pathlib import Path
import shutil
import sys
import tarfile
def digest(path):
with path.open('rb') as stream:
return digest_stream(stream)
def digest_stream(stream):
value = hashlib.sha256()
for chunk in iter(lambda: stream.read(1024 * 1024), b''):
value.update(chunk)
return value.hexdigest()
def main():
action, prefix_text, *args = sys.argv[1:]
prefix = Path(prefix_text)
if not prefix.is_absolute() or prefix == Path('/'):
raise SystemExit('需要非根绝对安装路径')
prefix = prefix.resolve()
manifest = prefix / 'share/qingjian/install-manifest.json'
old = json.loads(manifest.read_text()) if manifest.is_file() else {}
if action == 'uninstall':
for filename, checksum in old.items():
path = Path(filename)
if path.is_file() and not path.is_symlink() and digest(path) == checksum:
path.unlink()
elif path.exists():
print(f'保留已修改文件:{path}')
manifest.unlink(missing_ok=True)
return
if len(args) not in (5, 6):
raise SystemExit('install 需要仓库、服务、设置程序、插件、样例标记及可选框架版本')
root, server, settings, plugin, sample = args[:5]
flavor = args[5] if len(args) == 6 else 'fcitx5'
if flavor not in ('fcitx4', 'fcitx5'):
raise SystemExit('框架版本必须是 fcitx4 或 fcitx5')
root = Path(root)
data = Path(os.environ.get('XDG_DATA_HOME', str(Path.home() / '.local/share')))
if not data.is_absolute():
raise SystemExit('XDG_DATA_HOME 必须是绝对路径')
files = {prefix / 'share/licenses/qingjian/LICENSE': root / 'LICENSE',
prefix / 'share/licenses/qingjian/LICENSE-CNS11643.txt': root / 'assets/stroke/LICENSE-CNS11643.txt',
prefix / 'bin/qingjian-linux-server': Path(server),
prefix / 'bin/qingjian-settings': Path(settings),
data / 'applications/qingjian-settings.desktop': root / 'apps/settings/linux/qingjian-settings.desktop'}
# 托盘按名义尺寸挑图,目录名与像素必须一致:源图 1024px 不能直接装进 128x128(#547)。
for size in (16, 22, 24, 32, 48, 64, 128, 256):
files[data / f'icons/hicolor/{size}x{size}/apps/qingjian.png'] = (
root / f'assets/icon/linux/hicolor/{size}x{size}/apps/qingjian.png')
if flavor == 'fcitx4':
config = Path(os.environ.get('XDG_CONFIG_HOME', str(Path.home() / '.config')))
if not config.is_absolute():
raise SystemExit('XDG_CONFIG_HOME 必须是绝对路径')
icon = root / 'assets/icon/fcitx4.png'
files[prefix / 'lib/fcitx/fcitx-qingjian.so'] = Path(plugin)
files[config / 'fcitx/addon/fcitx-qingjian.conf'] = root / 'apps/linux/fcitx4/data/addon/fcitx-qingjian.conf'
files[config / 'fcitx/imicon/qingjian.png'] = icon
files[data / 'icons/hicolor/48x48/apps/fcitx-qingjian.png'] = icon
else:
files[prefix / 'lib/fcitx5/qingjian.so'] = Path(plugin)
for kind in ('addon', 'inputmethod'):
files[data / f'fcitx5/{kind}/qingjian.conf'] = root / f'apps/linux/fcitx5/data/{kind}/qingjian.conf'
resources = prefix / 'share/qingjian/resources'
for kind in ('sample', 'glossary', 'levels', 'emoji'):
for source in (root / 'assets' / kind).rglob('*'):
if source.is_file():
files[resources / source.relative_to(root)] = source
if sample != 'true':
generated = root / 'data/generated'
dictionary = generated / 'dict.qj'
archive = root / 'target/release-data/qingjian-data.tar.gz'
lock = dict(line.strip().split(' = ', 1) for line in (root / 'tools/release/data.lock').read_text().splitlines() if ' = ' in line)
if not dictionary.is_file() or not archive.is_file():
raise SystemExit('缺少产品数据,请先运行 tools/release/data-fetch.sh,或用 --sample 体验样例词库')
if digest(archive) != lock.get('qingjian-data.tar.gz'):
raise SystemExit('产品数据包与 tools/release/data.lock 校验值不符')
verified = {}
with tarfile.open(archive) as bundle:
for member in bundle.getmembers():
if not member.isfile():
continue
source = (root / member.name).resolve()
if not source.is_relative_to((root / 'data').resolve()):
raise SystemExit('数据包路径越界')
checksum = digest_stream(bundle.extractfile(member))
if not source.is_file() or digest(source) != checksum:
raise SystemExit(f'产品数据校验失败:{member.name}')
verified[source] = checksum
wanted = ('dict.qj', 'lm.qj', 'lm-unigram.tsv', 'lm-bigram.tsv', 'english.tsv')
for source in generated.rglob('*'):
if source.is_file() and not source.name.startswith('._') and (source.name in wanted or source.name.startswith('glossary-') or source.parent.name in ('dicts', 'codes')):
if source.resolve() not in verified:
raise SystemExit(f'产品数据没有校验记录:{source}')
files[resources / source.relative_to(root)] = source
model = root / 'data/models/hanzhang-zhiwei/hanzhang-zhiwei-small.qjm'
if not model.is_file():
raise SystemExit('缺少含章·知微 hanzhang-zhiwei-small.qjm,请先运行 tools/release/data-fetch.sh,或用 --sample 体验样例词库')
if verified.get(model.resolve()) != digest(model):
raise SystemExit('含章·知微不在已校验的数据包中')
files[resources / model.relative_to(root)] = model
p2c_model = root / 'data/models/hanzhang-tongbian/hanzhang-tongbian-small.qjm'
if not p2c_model.is_file() or verified.get(p2c_model.resolve()) != digest(p2c_model):
raise SystemExit('含章·通变缺失或不在已校验的数据包中')
files[resources / p2c_model.relative_to(root)] = p2c_model
# 安装前先检查所有目标,避免覆盖其他来源的同名文件。
for target in files:
if target.is_symlink() or (target.exists() and (str(target) not in old or digest(target) != old[str(target)])):
raise SystemExit(f'目标已存在且不属于本次安装:{target}')
installed = {}
for target, source in files.items():
target.parent.mkdir(parents=True, exist_ok=True)
temporary = target.with_name(target.name + '.qingjian-tmp')
shutil.copy2(source, temporary)
if target == data / 'fcitx5/addon/qingjian.conf':
temporary.write_text(temporary.read_text().replace('Library=qingjian\n', f'Library={prefix}/lib/fcitx5/qingjian\n'))
elif flavor == 'fcitx4' and target == config / 'fcitx/addon/fcitx-qingjian.conf':
temporary.write_text(temporary.read_text().replace('Library=fcitx-qingjian.so\n', f'Library={prefix}/lib/fcitx/fcitx-qingjian.so\n'))
if target == data / 'applications/qingjian-settings.desktop':
temporary.write_text(temporary.read_text().replace('Exec=qingjian-settings\n', f'Exec={prefix}/bin/qingjian-settings\n'))
temporary.replace(target)
installed[str(target)] = digest(target)
for filename, checksum in old.items():
path = Path(filename)
if filename not in installed and path.is_file() and not path.is_symlink() and digest(path) == checksum:
path.unlink()
manifest.parent.mkdir(parents=True, exist_ok=True)
manifest.write_text(json.dumps(installed, ensure_ascii=False, indent=2) + '\n')
if __name__ == '__main__':
main()