Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Generate explicit lockfiles per environment #898

Merged
merged 5 commits into from
Nov 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CONSTRUCT.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
39 changes: 39 additions & 0 deletions constructor/build_outputs.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__)


Expand Down Expand Up @@ -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 <env> --file <this 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:
Expand Down Expand Up @@ -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,
}
2 changes: 2 additions & 0 deletions constructor/construct.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
15 changes: 9 additions & 6 deletions constructor/fcp.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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
2 changes: 2 additions & 0 deletions docs/source/construct-yaml.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
3 changes: 3 additions & 0 deletions examples/extra_envs/construct.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ build_outputs:
- pkgs_list
- pkgs_list:
env: py310
- lockfile
- lockfile:
env: py310
- licenses:
include_text: True
text_errors: replace
Expand Down
19 changes: 19 additions & 0 deletions news/898-lockfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
### Enhancements

* Add new `lockfile` output in `build_outputs`. This generates a `@EXPLICIT` lockfile for the requested environment. (#898)

### Bug fixes

* <news item>

### Deprecations

* <news item>

### Docs

* <news item>

### Other

* <news item>
Loading