Пример #1
0
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)
Пример #2
0
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)
Пример #3
0
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)