Skip to content

Commit

Permalink
Generate explicit lockfiles per environment (#898)
Browse files Browse the repository at this point in the history
  • Loading branch information
jaimergp authored Nov 17, 2024
1 parent 4d659e0 commit 9e323e0
Show file tree
Hide file tree
Showing 7 changed files with 76 additions and 6 deletions.
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>

0 comments on commit 9e323e0

Please sign in to comment.