Files
WebAVP/scripts/package-mac-zip.py
T

54 lines
2.1 KiB
Python

"""Package an existing macOS app with Unix symlinks intact (stdlib only)."""
import argparse
import os
from pathlib import Path
import stat
import time
import zipfile
def package(app, output):
app = Path(app).resolve()
output = Path(output).resolve()
assert app.is_dir() and app.suffix == '.app'
with zipfile.ZipFile(output, 'w', compression=zipfile.ZIP_DEFLATED, compresslevel=6) as archive:
def add(path):
st = path.lstat()
name = path.relative_to(app.parent).as_posix()
is_dir = stat.S_ISDIR(st.st_mode)
info = zipfile.ZipInfo(name + ('/' if is_dir else ''), time.localtime(st.st_mtime)[:6])
info.create_system = 3
info.external_attr = st.st_mode << 16
if is_dir:
info.external_attr |= 0x10
info.compress_type = zipfile.ZIP_STORED if is_dir else zipfile.ZIP_DEFLATED
if stat.S_ISLNK(st.st_mode):
archive.writestr(info, os.readlink(path).encode('utf-8'))
elif is_dir:
archive.writestr(info, b'')
for child in sorted(path.iterdir()):
add(child)
elif stat.S_ISREG(st.st_mode):
with path.open('rb') as src, archive.open(info, 'w') as dst:
while chunk := src.read(1024 * 1024):
dst.write(chunk)
else:
raise ValueError(f'Unsupported file type: {path}')
add(app)
with zipfile.ZipFile(output) as archive:
assert archive.testzip() is None
for item in archive.infolist():
source = app.parent / item.filename.rstrip('/')
assert (item.external_attr >> 16) == source.lstat().st_mode
if source.is_symlink():
assert archive.read(item).decode('utf-8') == os.readlink(source)
print(output)
if __name__ == '__main__':
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('app')
parser.add_argument('output')
args = parser.parse_args()
package(args.app, args.output)