54 lines
2.1 KiB
Python
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)
|