diff --git a/CONSTRUCT.md b/CONSTRUCT.md index 1c978d434..ed5e3ad66 100644 --- a/CONSTRUCT.md +++ b/CONSTRUCT.md @@ -860,6 +860,8 @@ Allowed keys are: - `info.json`: The internal `info` object, serialized to JSON. Takes no options. - `pkgs_list`: The list of packages contained in a given environment. Options: - `env` (optional, default=`base`): Name of an environment in `extra_envs` to export. +- `lockfile`: An `@EXPLICIT` lockfile for a given environment. Options: + - `env` (optional, default=`base`): Name of an environment in `extra_envs` to export. - `licenses`: Generate a JSON file with the licensing details of all included packages. Options: - `include_text` (optional bool, default=`False`): Whether to dump the license text in the JSON. If false, only the path will be included. diff --git a/constructor/build_outputs.py b/constructor/build_outputs.py index 94c065b34..7bf258b18 100644 --- a/constructor/build_outputs.py +++ b/constructor/build_outputs.py @@ -10,6 +10,12 @@ from collections import defaultdict from pathlib import Path +from conda.base.constants import UNKNOWN_CHANNEL +from conda.common.url import remove_auth, split_anaconda_token +from conda.core.prefix_data import PrefixGraph + +from . import __version__ + logger = logging.getLogger(__name__) @@ -86,6 +92,38 @@ def dump_packages_list(info, env="base"): return os.path.abspath(outpath) +def dump_lockfile(info, env="base"): + if env == "base": + records = info["_records"] + elif env in info["_extra_envs_info"]: + records = info["_extra_envs_info"][env]["_records"] + else: + raise ValueError(f"env='{env}' is not a valid env name.") + lines = [ + "# This file may be used to create an environment using:", + "# $ conda create --name --file ", + f"# installer-name: {info['name']}", + f"# installer-version: {info['version']}", + f"# env-name: {env}", + f"# platform: {info['_platform']}", + f"# created-by: constructor {__version__}", + "@EXPLICIT" + ] + for record in PrefixGraph(records).graph: + url = record.get("url") + if not url or url.startswith(UNKNOWN_CHANNEL): + print("# no URL for: {}".format(record["fn"])) + continue + url = remove_auth(split_anaconda_token(url)[0]) + hash_value = record.get("md5") + lines.append(url + (f"#{hash_value}" if hash_value else "")) + + outpath = os.path.join(info["_output_dir"], f'lockfile.{env}.txt') + with open(outpath, 'w') as f: + f.write("\n".join(lines)) + return os.path.abspath(outpath) + + def dump_licenses(info, include_text=False, text_errors=None): """ Create a JSON document with a mapping with schema: @@ -140,5 +178,6 @@ def dump_licenses(info, include_text=False, text_errors=None): "hash": dump_hash, "info.json": dump_info, "pkgs_list": dump_packages_list, + "lockfile": dump_lockfile, "licenses": dump_licenses, } diff --git a/constructor/construct.py b/constructor/construct.py index 210c157c5..01ddff0e5 100644 --- a/constructor/construct.py +++ b/constructor/construct.py @@ -635,6 +635,8 @@ - `info.json`: The internal `info` object, serialized to JSON. Takes no options. - `pkgs_list`: The list of packages contained in a given environment. Options: - `env` (optional, default=`base`): Name of an environment in `extra_envs` to export. +- `lockfile`: An `@EXPLICIT` lockfile for a given environment. Options: + - `env` (optional, default=`base`): Name of an environment in `extra_envs` to export. - `licenses`: Generate a JSON file with the licensing details of all included packages. Options: - `include_text` (optional bool, default=`False`): Whether to dump the license text in the JSON. If false, only the path will be included. diff --git a/constructor/fcp.py b/constructor/fcp.py index e6f66dfa1..1ba2c605c 100644 --- a/constructor/fcp.py +++ b/constructor/fcp.py @@ -403,7 +403,7 @@ def _main(name, version, download_dir, platform, channel_urls=(), channels_remap env_pc_recs, env_urls, env_dists, _ = _fetch_precs( env_precs, download_dir, transmute_file_type=transmute_file_type ) - extra_envs_data[env_name] = {"_urls": env_urls, "_dists": env_dists} + extra_envs_data[env_name] = {"_urls": env_urls, "_dists": env_dists, "_records": env_precs} all_pc_recs += env_pc_recs duplicate_files = "warn" if ignore_duplicate_files else "error" @@ -418,6 +418,7 @@ def _main(name, version, download_dir, platform, channel_urls=(), channels_remap return ( all_pc_recs, + precs, _urls, dists, approx_tarballs_size, @@ -466,8 +467,9 @@ def main(info, verbose=True, dry_run=False, conda_exe="conda.exe"): ( pkg_records, - _urls, - dists, + _base_env_records, + _base_env_urls, + _base_env_dists, approx_tarballs_size, approx_pkgs_size, has_conda, @@ -495,10 +497,11 @@ def main(info, verbose=True, dry_run=False, conda_exe="conda.exe"): ) info["_all_pkg_records"] = pkg_records # full PackageRecord objects - info["_urls"] = _urls # needed to mock the repodata cache - info["_dists"] = dists # needed to tell conda what to install + info["_urls"] = _base_env_urls # needed to mock the repodata cache + info["_dists"] = _base_env_dists # needed to tell conda what to install + info["_records"] = _base_env_records # needed to generate optional lockfile info["_approx_tarballs_size"] = approx_tarballs_size info["_approx_pkgs_size"] = approx_pkgs_size info["_has_conda"] = has_conda - # contains {env_name: [_dists, _urls]} for each extra environment + # contains {env_name: [_dists, _urls, _records]} for each extra environment info["_extra_envs_info"] = extra_envs_info diff --git a/docs/source/construct-yaml.md b/docs/source/construct-yaml.md index 1c978d434..ed5e3ad66 100644 --- a/docs/source/construct-yaml.md +++ b/docs/source/construct-yaml.md @@ -860,6 +860,8 @@ Allowed keys are: - `info.json`: The internal `info` object, serialized to JSON. Takes no options. - `pkgs_list`: The list of packages contained in a given environment. Options: - `env` (optional, default=`base`): Name of an environment in `extra_envs` to export. +- `lockfile`: An `@EXPLICIT` lockfile for a given environment. Options: + - `env` (optional, default=`base`): Name of an environment in `extra_envs` to export. - `licenses`: Generate a JSON file with the licensing details of all included packages. Options: - `include_text` (optional bool, default=`False`): Whether to dump the license text in the JSON. If false, only the path will be included. diff --git a/examples/extra_envs/construct.yaml b/examples/extra_envs/construct.yaml index d39c72292..b839b9239 100644 --- a/examples/extra_envs/construct.yaml +++ b/examples/extra_envs/construct.yaml @@ -31,6 +31,9 @@ build_outputs: - pkgs_list - pkgs_list: env: py310 + - lockfile + - lockfile: + env: py310 - licenses: include_text: True text_errors: replace diff --git a/news/898-lockfile b/news/898-lockfile new file mode 100644 index 000000000..bd0da1f88 --- /dev/null +++ b/news/898-lockfile @@ -0,0 +1,19 @@ +### Enhancements + +* Add new `lockfile` output in `build_outputs`. This generates a `@EXPLICIT` lockfile for the requested environment. (#898) + +### Bug fixes + +* + +### Deprecations + +* + +### Docs + +* + +### Other + +*