def create_archive(source: Path, target: Path, interpreter: str, main: str, compressed: bool = True) -> None: """Create an application archive from SOURCE. A slightly modified version of stdlib's `zipapp.create_archive <https://docs.python.org/3/library/zipapp.html#zipapp.create_archive>`_ """ # Check that main has the right format. mod, sep, fn = main.partition(":") mod_ok = all(part.isidentifier() for part in mod.split(".")) fn_ok = all(part.isidentifier() for part in fn.split(".")) if not (sep == ":" and mod_ok and fn_ok): raise zipapp.ZipAppError("Invalid entry point: " + main) main_py = MAIN_TEMPLATE.format(module=mod, fn=fn) with maybe_open(target, "wb") as fd: # write shebang write_file_prefix(fd, interpreter) # determine compression compression = zipfile.ZIP_DEFLATED if compressed else zipfile.ZIP_STORED # create zipapp with zipfile.ZipFile(fd, "w", compression=compression) as z: for child in source.rglob("*"): # skip compiled files if child.suffix == '.pyc': continue arcname = child.relative_to(source) z.write(str(child), str(arcname)) # write main z.writestr("__main__.py", main_py.encode("utf-8")) # make executable # NOTE on windows this is no-op target.chmod(target.stat().st_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH)
def create_archive(sources: List[Path], target: Path, interpreter: str, main: str, env: Environment, compressed: bool = True) -> None: """Create an application archive from SOURCE. This function is a heavily modified version of stdlib's `zipapp.create_archive <https://docs.python.org/3/library/zipapp.html#zipapp.create_archive>`_ """ # Check that main has the right format. mod, sep, fn = main.partition(":") mod_ok = all(part.isidentifier() for part in mod.split(".")) fn_ok = all(part.isidentifier() for part in fn.split(".")) if not (sep == ":" and mod_ok and fn_ok): raise zipapp.ZipAppError("Invalid entry point: " + main) # Collect our timestamp data main_py = MAIN_TEMPLATE.format(module=mod, fn=fn) timestamp = datetime.strptime( env.built_at, BUILD_AT_TIMESTAMP_FORMAT).replace(tzinfo=timezone.utc).timestamp() zipinfo_datetime: Tuple[int, int, int, int, int, int] = time.gmtime(int(timestamp))[0:6] with target.open(mode="wb") as fd: # Write shebang. write_file_prefix(fd, interpreter) # Determine compression. compression = zipfile.ZIP_DEFLATED if compressed else zipfile.ZIP_STORED # Pack zipapp with dependencies. with zipfile.ZipFile(fd, "w", compression=compression) as archive: site_packages = Path("site-packages") contents_hash = hashlib.sha256() for source in sources: # Glob is known to return results in non-deterministic order. # We need to sort them by in-archive paths to ensure # that archive contents are reproducible. for path in sorted(source.rglob("*"), key=str): # Skip compiled files and directories (as they are not required to be present in the zip). if path.suffix == ".pyc" or path.is_dir(): continue data = path.read_bytes() # update the contents hash contents_hash.update(data) arcname = str(site_packages / path.relative_to(source)) write_to_zipapp(archive, arcname, data, zipinfo_datetime, compression, stat=path.stat()) if env.build_id is None: # Now that we have a hash of all the source files, use it as our build id if the user did not # specify a custom one. env.build_id = contents_hash.hexdigest() # now let's add the shiv bootstrap code. bootstrap_target = Path("_bootstrap") for bootstrap_file in importlib_resources.contents( bootstrap): # type: ignore if importlib_resources.is_resource( bootstrap, bootstrap_file): # type: ignore with importlib_resources.path( bootstrap, bootstrap_file) as path: # type: ignore data = path.read_bytes() write_to_zipapp( archive, str(bootstrap_target / path.name), data, zipinfo_datetime, compression, stat=path.stat(), ) # Write environment info in json file. # # The environment file contains build_id which is a SHA-256 checksum of all **site-packages** contents. # the bootstarp code, environment.json and __main__.py are not used to calculate the checksum, is it's # only used for local caching of site-packages and these files are always read from archive. write_to_zipapp(archive, "environment.json", env.to_json().encode("utf-8"), zipinfo_datetime, compression) # write __main__ write_to_zipapp(archive, "__main__.py", main_py.encode("utf-8"), zipinfo_datetime, compression) # Make pyz executable (on windows this is no-op). target.chmod(target.stat().st_mode | S_IXUSR | S_IXGRP | S_IXOTH)
def create_archive(source: Path, target: Path, interpreter: str, main: str, env: Environment, compressed: bool = True) -> None: """Create an application archive from SOURCE. A modified version of stdlib's `zipapp.create_archive <https://docs.python.org/3/library/zipapp.html#zipapp.create_archive>`_ """ # Check that main has the right format. mod, sep, fn = main.partition(":") mod_ok = all(part.isidentifier() for part in mod.split(".")) fn_ok = all(part.isidentifier() for part in fn.split(".")) if not (sep == ":" and mod_ok and fn_ok): raise zipapp.ZipAppError("Invalid entry point: " + main) main_py = MAIN_TEMPLATE.format(module=mod, fn=fn) with maybe_open(target, "wb") as fd: # Write shebang. write_file_prefix(fd, interpreter) # Determine compression. compression = zipfile.ZIP_DEFLATED if compressed else zipfile.ZIP_STORED # Pack zipapp with dependencies. with zipfile.ZipFile(fd, "w", compression=compression) as z: site_packages = Path("site-packages") # Glob is known to return results in undetermenistic order. # We need to sort them by in-archive paths to ensure # that archive contents are reproducible. for child in sorted(source.rglob("*"), key=str): # Skip compiled files. if child.suffix == ".pyc": continue arcname = site_packages / child.relative_to(source) z.write(child, arcname) bootstrap_target = Path("_bootstrap") # Write shiv's bootstrap code. for bootstrap_file in importlib_resources.contents( bootstrap): # type: ignore if importlib_resources.is_resource( bootstrap, bootstrap_file): # type: ignore with importlib_resources.path( bootstrap, bootstrap_file) as f: # type: ignore z.write(f.absolute(), bootstrap_target / f.name) # write environment z.writestr("environment.json", env.to_json().encode("utf-8")) # write main z.writestr("__main__.py", main_py.encode("utf-8")) # Make pyz executable (on windows this is no-op). target.chmod(target.stat().st_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH)