xref: /aosp_15_r20/prebuilts/build-tools/common/py3-stdlib/zipapp.py (revision cda5da8d549138a6648c5ee6d7a49cf8f4a657be)
1*cda5da8dSAndroid Build Coastguard Workerimport contextlib
2*cda5da8dSAndroid Build Coastguard Workerimport os
3*cda5da8dSAndroid Build Coastguard Workerimport pathlib
4*cda5da8dSAndroid Build Coastguard Workerimport shutil
5*cda5da8dSAndroid Build Coastguard Workerimport stat
6*cda5da8dSAndroid Build Coastguard Workerimport sys
7*cda5da8dSAndroid Build Coastguard Workerimport zipfile
8*cda5da8dSAndroid Build Coastguard Worker
9*cda5da8dSAndroid Build Coastguard Worker__all__ = ['ZipAppError', 'create_archive', 'get_interpreter']
10*cda5da8dSAndroid Build Coastguard Worker
11*cda5da8dSAndroid Build Coastguard Worker
12*cda5da8dSAndroid Build Coastguard Worker# The __main__.py used if the users specifies "-m module:fn".
13*cda5da8dSAndroid Build Coastguard Worker# Note that this will always be written as UTF-8 (module and
14*cda5da8dSAndroid Build Coastguard Worker# function names can be non-ASCII in Python 3).
15*cda5da8dSAndroid Build Coastguard Worker# We add a coding cookie even though UTF-8 is the default in Python 3
16*cda5da8dSAndroid Build Coastguard Worker# because the resulting archive may be intended to be run under Python 2.
17*cda5da8dSAndroid Build Coastguard WorkerMAIN_TEMPLATE = """\
18*cda5da8dSAndroid Build Coastguard Worker# -*- coding: utf-8 -*-
19*cda5da8dSAndroid Build Coastguard Workerimport {module}
20*cda5da8dSAndroid Build Coastguard Worker{module}.{fn}()
21*cda5da8dSAndroid Build Coastguard Worker"""
22*cda5da8dSAndroid Build Coastguard Worker
23*cda5da8dSAndroid Build Coastguard Worker
24*cda5da8dSAndroid Build Coastguard Worker# The Windows launcher defaults to UTF-8 when parsing shebang lines if the
25*cda5da8dSAndroid Build Coastguard Worker# file has no BOM. So use UTF-8 on Windows.
26*cda5da8dSAndroid Build Coastguard Worker# On Unix, use the filesystem encoding.
27*cda5da8dSAndroid Build Coastguard Workerif sys.platform.startswith('win'):
28*cda5da8dSAndroid Build Coastguard Worker    shebang_encoding = 'utf-8'
29*cda5da8dSAndroid Build Coastguard Workerelse:
30*cda5da8dSAndroid Build Coastguard Worker    shebang_encoding = sys.getfilesystemencoding()
31*cda5da8dSAndroid Build Coastguard Worker
32*cda5da8dSAndroid Build Coastguard Worker
33*cda5da8dSAndroid Build Coastguard Workerclass ZipAppError(ValueError):
34*cda5da8dSAndroid Build Coastguard Worker    pass
35*cda5da8dSAndroid Build Coastguard Worker
36*cda5da8dSAndroid Build Coastguard Worker
37*cda5da8dSAndroid Build Coastguard Worker@contextlib.contextmanager
38*cda5da8dSAndroid Build Coastguard Workerdef _maybe_open(archive, mode):
39*cda5da8dSAndroid Build Coastguard Worker    if isinstance(archive, (str, os.PathLike)):
40*cda5da8dSAndroid Build Coastguard Worker        with open(archive, mode) as f:
41*cda5da8dSAndroid Build Coastguard Worker            yield f
42*cda5da8dSAndroid Build Coastguard Worker    else:
43*cda5da8dSAndroid Build Coastguard Worker        yield archive
44*cda5da8dSAndroid Build Coastguard Worker
45*cda5da8dSAndroid Build Coastguard Worker
46*cda5da8dSAndroid Build Coastguard Workerdef _write_file_prefix(f, interpreter):
47*cda5da8dSAndroid Build Coastguard Worker    """Write a shebang line."""
48*cda5da8dSAndroid Build Coastguard Worker    if interpreter:
49*cda5da8dSAndroid Build Coastguard Worker        shebang = b'#!' + interpreter.encode(shebang_encoding) + b'\n'
50*cda5da8dSAndroid Build Coastguard Worker        f.write(shebang)
51*cda5da8dSAndroid Build Coastguard Worker
52*cda5da8dSAndroid Build Coastguard Worker
53*cda5da8dSAndroid Build Coastguard Workerdef _copy_archive(archive, new_archive, interpreter=None):
54*cda5da8dSAndroid Build Coastguard Worker    """Copy an application archive, modifying the shebang line."""
55*cda5da8dSAndroid Build Coastguard Worker    with _maybe_open(archive, 'rb') as src:
56*cda5da8dSAndroid Build Coastguard Worker        # Skip the shebang line from the source.
57*cda5da8dSAndroid Build Coastguard Worker        # Read 2 bytes of the source and check if they are #!.
58*cda5da8dSAndroid Build Coastguard Worker        first_2 = src.read(2)
59*cda5da8dSAndroid Build Coastguard Worker        if first_2 == b'#!':
60*cda5da8dSAndroid Build Coastguard Worker            # Discard the initial 2 bytes and the rest of the shebang line.
61*cda5da8dSAndroid Build Coastguard Worker            first_2 = b''
62*cda5da8dSAndroid Build Coastguard Worker            src.readline()
63*cda5da8dSAndroid Build Coastguard Worker
64*cda5da8dSAndroid Build Coastguard Worker        with _maybe_open(new_archive, 'wb') as dst:
65*cda5da8dSAndroid Build Coastguard Worker            _write_file_prefix(dst, interpreter)
66*cda5da8dSAndroid Build Coastguard Worker            # If there was no shebang, "first_2" contains the first 2 bytes
67*cda5da8dSAndroid Build Coastguard Worker            # of the source file, so write them before copying the rest
68*cda5da8dSAndroid Build Coastguard Worker            # of the file.
69*cda5da8dSAndroid Build Coastguard Worker            dst.write(first_2)
70*cda5da8dSAndroid Build Coastguard Worker            shutil.copyfileobj(src, dst)
71*cda5da8dSAndroid Build Coastguard Worker
72*cda5da8dSAndroid Build Coastguard Worker    if interpreter and isinstance(new_archive, str):
73*cda5da8dSAndroid Build Coastguard Worker        os.chmod(new_archive, os.stat(new_archive).st_mode | stat.S_IEXEC)
74*cda5da8dSAndroid Build Coastguard Worker
75*cda5da8dSAndroid Build Coastguard Worker
76*cda5da8dSAndroid Build Coastguard Workerdef create_archive(source, target=None, interpreter=None, main=None,
77*cda5da8dSAndroid Build Coastguard Worker                   filter=None, compressed=False):
78*cda5da8dSAndroid Build Coastguard Worker    """Create an application archive from SOURCE.
79*cda5da8dSAndroid Build Coastguard Worker
80*cda5da8dSAndroid Build Coastguard Worker    The SOURCE can be the name of a directory, or a filename or a file-like
81*cda5da8dSAndroid Build Coastguard Worker    object referring to an existing archive.
82*cda5da8dSAndroid Build Coastguard Worker
83*cda5da8dSAndroid Build Coastguard Worker    The content of SOURCE is packed into an application archive in TARGET,
84*cda5da8dSAndroid Build Coastguard Worker    which can be a filename or a file-like object.  If SOURCE is a directory,
85*cda5da8dSAndroid Build Coastguard Worker    TARGET can be omitted and will default to the name of SOURCE with .pyz
86*cda5da8dSAndroid Build Coastguard Worker    appended.
87*cda5da8dSAndroid Build Coastguard Worker
88*cda5da8dSAndroid Build Coastguard Worker    The created application archive will have a shebang line specifying
89*cda5da8dSAndroid Build Coastguard Worker    that it should run with INTERPRETER (there will be no shebang line if
90*cda5da8dSAndroid Build Coastguard Worker    INTERPRETER is None), and a __main__.py which runs MAIN (if MAIN is
91*cda5da8dSAndroid Build Coastguard Worker    not specified, an existing __main__.py will be used).  It is an error
92*cda5da8dSAndroid Build Coastguard Worker    to specify MAIN for anything other than a directory source with no
93*cda5da8dSAndroid Build Coastguard Worker    __main__.py, and it is an error to omit MAIN if the directory has no
94*cda5da8dSAndroid Build Coastguard Worker    __main__.py.
95*cda5da8dSAndroid Build Coastguard Worker    """
96*cda5da8dSAndroid Build Coastguard Worker    # Are we copying an existing archive?
97*cda5da8dSAndroid Build Coastguard Worker    source_is_file = False
98*cda5da8dSAndroid Build Coastguard Worker    if hasattr(source, 'read') and hasattr(source, 'readline'):
99*cda5da8dSAndroid Build Coastguard Worker        source_is_file = True
100*cda5da8dSAndroid Build Coastguard Worker    else:
101*cda5da8dSAndroid Build Coastguard Worker        source = pathlib.Path(source)
102*cda5da8dSAndroid Build Coastguard Worker        if source.is_file():
103*cda5da8dSAndroid Build Coastguard Worker            source_is_file = True
104*cda5da8dSAndroid Build Coastguard Worker
105*cda5da8dSAndroid Build Coastguard Worker    if source_is_file:
106*cda5da8dSAndroid Build Coastguard Worker        _copy_archive(source, target, interpreter)
107*cda5da8dSAndroid Build Coastguard Worker        return
108*cda5da8dSAndroid Build Coastguard Worker
109*cda5da8dSAndroid Build Coastguard Worker    # We are creating a new archive from a directory.
110*cda5da8dSAndroid Build Coastguard Worker    if not source.exists():
111*cda5da8dSAndroid Build Coastguard Worker        raise ZipAppError("Source does not exist")
112*cda5da8dSAndroid Build Coastguard Worker    has_main = (source / '__main__.py').is_file()
113*cda5da8dSAndroid Build Coastguard Worker    if main and has_main:
114*cda5da8dSAndroid Build Coastguard Worker        raise ZipAppError(
115*cda5da8dSAndroid Build Coastguard Worker            "Cannot specify entry point if the source has __main__.py")
116*cda5da8dSAndroid Build Coastguard Worker    if not (main or has_main):
117*cda5da8dSAndroid Build Coastguard Worker        raise ZipAppError("Archive has no entry point")
118*cda5da8dSAndroid Build Coastguard Worker
119*cda5da8dSAndroid Build Coastguard Worker    main_py = None
120*cda5da8dSAndroid Build Coastguard Worker    if main:
121*cda5da8dSAndroid Build Coastguard Worker        # Check that main has the right format.
122*cda5da8dSAndroid Build Coastguard Worker        mod, sep, fn = main.partition(':')
123*cda5da8dSAndroid Build Coastguard Worker        mod_ok = all(part.isidentifier() for part in mod.split('.'))
124*cda5da8dSAndroid Build Coastguard Worker        fn_ok = all(part.isidentifier() for part in fn.split('.'))
125*cda5da8dSAndroid Build Coastguard Worker        if not (sep == ':' and mod_ok and fn_ok):
126*cda5da8dSAndroid Build Coastguard Worker            raise ZipAppError("Invalid entry point: " + main)
127*cda5da8dSAndroid Build Coastguard Worker        main_py = MAIN_TEMPLATE.format(module=mod, fn=fn)
128*cda5da8dSAndroid Build Coastguard Worker
129*cda5da8dSAndroid Build Coastguard Worker    if target is None:
130*cda5da8dSAndroid Build Coastguard Worker        target = source.with_suffix('.pyz')
131*cda5da8dSAndroid Build Coastguard Worker    elif not hasattr(target, 'write'):
132*cda5da8dSAndroid Build Coastguard Worker        target = pathlib.Path(target)
133*cda5da8dSAndroid Build Coastguard Worker
134*cda5da8dSAndroid Build Coastguard Worker    with _maybe_open(target, 'wb') as fd:
135*cda5da8dSAndroid Build Coastguard Worker        _write_file_prefix(fd, interpreter)
136*cda5da8dSAndroid Build Coastguard Worker        compression = (zipfile.ZIP_DEFLATED if compressed else
137*cda5da8dSAndroid Build Coastguard Worker                       zipfile.ZIP_STORED)
138*cda5da8dSAndroid Build Coastguard Worker        with zipfile.ZipFile(fd, 'w', compression=compression) as z:
139*cda5da8dSAndroid Build Coastguard Worker            for child in source.rglob('*'):
140*cda5da8dSAndroid Build Coastguard Worker                arcname = child.relative_to(source)
141*cda5da8dSAndroid Build Coastguard Worker                if filter is None or filter(arcname):
142*cda5da8dSAndroid Build Coastguard Worker                    z.write(child, arcname.as_posix())
143*cda5da8dSAndroid Build Coastguard Worker            if main_py:
144*cda5da8dSAndroid Build Coastguard Worker                z.writestr('__main__.py', main_py.encode('utf-8'))
145*cda5da8dSAndroid Build Coastguard Worker
146*cda5da8dSAndroid Build Coastguard Worker    if interpreter and not hasattr(target, 'write'):
147*cda5da8dSAndroid Build Coastguard Worker        target.chmod(target.stat().st_mode | stat.S_IEXEC)
148*cda5da8dSAndroid Build Coastguard Worker
149*cda5da8dSAndroid Build Coastguard Worker
150*cda5da8dSAndroid Build Coastguard Workerdef get_interpreter(archive):
151*cda5da8dSAndroid Build Coastguard Worker    with _maybe_open(archive, 'rb') as f:
152*cda5da8dSAndroid Build Coastguard Worker        if f.read(2) == b'#!':
153*cda5da8dSAndroid Build Coastguard Worker            return f.readline().strip().decode(shebang_encoding)
154*cda5da8dSAndroid Build Coastguard Worker
155*cda5da8dSAndroid Build Coastguard Worker
156*cda5da8dSAndroid Build Coastguard Workerdef main(args=None):
157*cda5da8dSAndroid Build Coastguard Worker    """Run the zipapp command line interface.
158*cda5da8dSAndroid Build Coastguard Worker
159*cda5da8dSAndroid Build Coastguard Worker    The ARGS parameter lets you specify the argument list directly.
160*cda5da8dSAndroid Build Coastguard Worker    Omitting ARGS (or setting it to None) works as for argparse, using
161*cda5da8dSAndroid Build Coastguard Worker    sys.argv[1:] as the argument list.
162*cda5da8dSAndroid Build Coastguard Worker    """
163*cda5da8dSAndroid Build Coastguard Worker    import argparse
164*cda5da8dSAndroid Build Coastguard Worker
165*cda5da8dSAndroid Build Coastguard Worker    parser = argparse.ArgumentParser()
166*cda5da8dSAndroid Build Coastguard Worker    parser.add_argument('--output', '-o', default=None,
167*cda5da8dSAndroid Build Coastguard Worker            help="The name of the output archive. "
168*cda5da8dSAndroid Build Coastguard Worker                 "Required if SOURCE is an archive.")
169*cda5da8dSAndroid Build Coastguard Worker    parser.add_argument('--python', '-p', default=None,
170*cda5da8dSAndroid Build Coastguard Worker            help="The name of the Python interpreter to use "
171*cda5da8dSAndroid Build Coastguard Worker                 "(default: no shebang line).")
172*cda5da8dSAndroid Build Coastguard Worker    parser.add_argument('--main', '-m', default=None,
173*cda5da8dSAndroid Build Coastguard Worker            help="The main function of the application "
174*cda5da8dSAndroid Build Coastguard Worker                 "(default: use an existing __main__.py).")
175*cda5da8dSAndroid Build Coastguard Worker    parser.add_argument('--compress', '-c', action='store_true',
176*cda5da8dSAndroid Build Coastguard Worker            help="Compress files with the deflate method. "
177*cda5da8dSAndroid Build Coastguard Worker                 "Files are stored uncompressed by default.")
178*cda5da8dSAndroid Build Coastguard Worker    parser.add_argument('--info', default=False, action='store_true',
179*cda5da8dSAndroid Build Coastguard Worker            help="Display the interpreter from the archive.")
180*cda5da8dSAndroid Build Coastguard Worker    parser.add_argument('source',
181*cda5da8dSAndroid Build Coastguard Worker            help="Source directory (or existing archive).")
182*cda5da8dSAndroid Build Coastguard Worker
183*cda5da8dSAndroid Build Coastguard Worker    args = parser.parse_args(args)
184*cda5da8dSAndroid Build Coastguard Worker
185*cda5da8dSAndroid Build Coastguard Worker    # Handle `python -m zipapp archive.pyz --info`.
186*cda5da8dSAndroid Build Coastguard Worker    if args.info:
187*cda5da8dSAndroid Build Coastguard Worker        if not os.path.isfile(args.source):
188*cda5da8dSAndroid Build Coastguard Worker            raise SystemExit("Can only get info for an archive file")
189*cda5da8dSAndroid Build Coastguard Worker        interpreter = get_interpreter(args.source)
190*cda5da8dSAndroid Build Coastguard Worker        print("Interpreter: {}".format(interpreter or "<none>"))
191*cda5da8dSAndroid Build Coastguard Worker        sys.exit(0)
192*cda5da8dSAndroid Build Coastguard Worker
193*cda5da8dSAndroid Build Coastguard Worker    if os.path.isfile(args.source):
194*cda5da8dSAndroid Build Coastguard Worker        if args.output is None or (os.path.exists(args.output) and
195*cda5da8dSAndroid Build Coastguard Worker                                   os.path.samefile(args.source, args.output)):
196*cda5da8dSAndroid Build Coastguard Worker            raise SystemExit("In-place editing of archives is not supported")
197*cda5da8dSAndroid Build Coastguard Worker        if args.main:
198*cda5da8dSAndroid Build Coastguard Worker            raise SystemExit("Cannot change the main function when copying")
199*cda5da8dSAndroid Build Coastguard Worker
200*cda5da8dSAndroid Build Coastguard Worker    create_archive(args.source, args.output,
201*cda5da8dSAndroid Build Coastguard Worker                   interpreter=args.python, main=args.main,
202*cda5da8dSAndroid Build Coastguard Worker                   compressed=args.compress)
203*cda5da8dSAndroid Build Coastguard Worker
204*cda5da8dSAndroid Build Coastguard Worker
205*cda5da8dSAndroid Build Coastguard Workerif __name__ == '__main__':
206*cda5da8dSAndroid Build Coastguard Worker    main()
207