Skip to content

Instantly share code, notes, and snippets.

@peterk87
Last active May 30, 2022 18:30
Show Gist options
  • Save peterk87/c28167ceffbb36dcbcf5b5b225c4043d to your computer and use it in GitHub Desktop.
Save peterk87/c28167ceffbb36dcbcf5b5b225c4043d to your computer and use it in GitHub Desktop.
Get "mulled-v2-{hash}" for multi-package containers (https://github.com/BioContainers/multi-package-containers) comma-delimited multi-package definitions (like in the hash.tsv file)
#!/usr/bin/env python
import argparse
import hashlib
import sys
from collections import namedtuple
from typing import List, Dict, Tuple, Union
# from https://github.com/galaxyproject/galaxy/blob/f12ee5ce6d602cd4c8b4cfc2112988b84a4f255e/lib/galaxy/tool_util/deps/mulled/util.py#L185
Target = namedtuple("Target", ["package_name", "version", "build", "package"])
# from https://github.com/galaxyproject/galaxy/blob/f12ee5ce6d602cd4c8b4cfc2112988b84a4f255e/lib/galaxy/tool_util/deps/mulled/util.py#L188
def build_target(package_name, version=None, build=None, tag=None):
"""Use supplied arguments to build a :class:`Target` object."""
if tag is not None:
assert version is None
assert build is None
version, build = split_tag(tag)
# conda package and quay image names are lowercase
return Target(package_name.lower(), version, build, package_name)
# from https://github.com/galaxyproject/galaxy/blob/f12ee5ce6d602cd4c8b4cfc2112988b84a4f255e/lib/galaxy/tool_util/deps/mulled/util.py#L210
def _simple_image_name(targets, image_build=None):
target = targets[0]
suffix = ""
if target.version is not None:
build = target.build
if build is None and image_build is not None and image_build != "0":
# Special case image_build == "0", which has been built without a suffix
print("WARNING: Hard-coding image build instead of using Conda build - this is not recommended.")
build = image_build
suffix += f":{target.version}"
if build is not None:
suffix += f"--{build}"
return f"{target.package_name}{suffix}"
# from https://github.com/galaxyproject/galaxy/blob/f12ee5ce6d602cd4c8b4cfc2112988b84a4f255e/lib/galaxy/tool_util/deps/mulled/util.py#L264
def v2_image_name(targets, image_build=None, name_override=None):
"""Generate mulled hash version 2 container identifier for supplied arguments.
If a single target is specified, simply use the supplied name and version as
the repository name and tag respectively. If multiple targets are supplied,
hash the package names as the repository name and hash the package versions (if set)
as the tag.
>>> single_targets = [build_target("samtools", version="1.3.1")]
>>> v2_image_name(single_targets)
'samtools:1.3.1'
>>> single_targets = [build_target("samtools", version="1.3.1", build="py_1")]
>>> v2_image_name(single_targets)
'samtools:1.3.1--py_1'
>>> single_targets = [build_target("samtools", version="1.3.1")]
>>> v2_image_name(single_targets, image_build="0")
'samtools:1.3.1'
>>> single_targets = [build_target("samtools", version="1.3.1", build="py_1")]
>>> v2_image_name(single_targets, image_build="0")
'samtools:1.3.1--py_1'
>>> multi_targets = [build_target("samtools", version="1.3.1"), build_target("bwa", version="0.7.13")]
>>> v2_image_name(multi_targets)
'mulled-v2-fe8faa35dbf6dc65a0f7f5d4ea12e31a79f73e40:4d0535c94ef45be8459f429561f0894c3fe0ebcf'
>>> multi_targets_on_versionless = [build_target("samtools", version="1.3.1"), build_target("bwa")]
>>> v2_image_name(multi_targets_on_versionless)
'mulled-v2-fe8faa35dbf6dc65a0f7f5d4ea12e31a79f73e40:b0c847e4fb89c343b04036e33b2daa19c4152cf5'
>>> multi_targets_versionless = [build_target("samtools"), build_target("bwa")]
>>> v2_image_name(multi_targets_versionless)
'mulled-v2-fe8faa35dbf6dc65a0f7f5d4ea12e31a79f73e40'
"""
if name_override is not None:
print(
"WARNING: Overriding mulled image name, auto-detection of 'mulled' package attributes will fail to detect result."
)
return name_override
targets = list(targets)
if len(targets) == 1:
return _simple_image_name(targets, image_build=image_build)
else:
targets_order = sorted(targets, key=lambda t: t.package_name)
package_name_buffer = "\n".join(map(lambda t: t.package_name, targets_order))
package_hash = hashlib.sha1()
package_hash.update(package_name_buffer.encode())
versions = map(lambda t: t.version, targets_order)
if any(versions):
# Only hash versions if at least one package has versions...
version_name_buffer = "\n".join(map(lambda t: t.version or "null", targets_order))
version_hash = hashlib.sha1()
version_hash.update(version_name_buffer.encode())
version_hash_str = version_hash.hexdigest()
else:
version_hash_str = ""
if not image_build:
build_suffix = ""
elif version_hash_str:
# tagged verson is <version_hash>-<build>
build_suffix = f"-{image_build}"
else:
# tagged version is simply the build
build_suffix = image_build
suffix = ""
if version_hash_str or build_suffix:
suffix = f":{version_hash_str}{build_suffix}"
return f"mulled-v2-{package_hash.hexdigest()}{suffix}"
def combo_line_to_build_targets(s: str) -> List[Target]:
"""List of Target tuples from multi-package comma-delimited definition
See https://github.com/BioContainers/multi-package-containers/blob/master/combinations/hash.tsv for multi-package definitions built into multi-package containers
"""
out = []
package = None
version = None
build = None
for package_version in s.split(','):
if '=' in package_version:
sp = package_version.split('=')
if len(sp) == 3:
package, version, build = sp
elif len(sp) == 2:
package, version = sp
else:
raise Exception(f'Could not parse package/version/build definition from "{package_version}" in "{s}"')
else:
package = package_version
version = None
out.append(
build_target(
package_name=package,
version=version,
build=build,
)
)
return out
def main():
parser = argparse.ArgumentParser(
description='Get "mulled-v2-{hash}" for multi-package containers (https://github.com/BioContainers/multi-package-containers) comma-delimited multi-package definitions (like in the hash.tsv file)',
)
parser.add_argument('packages', metavar='P', type=str, nargs='+',
help='Multi-package string (e.g. "minimap2=2.18,samtools=1.12")')
args = parser.parse_args()
if not args.packages:
print('No multi-package containers specified!')
sys.exit(1)
for p in args.packages:
targets = combo_line_to_build_targets(p)
print(f'Packages : {p}')
print(f'# packages : {len(targets)}')
mulled_name = v2_image_name(targets)
print(f'Mulled name : {mulled_name}')
if ':' in mulled_name:
packages_hash, versions_hash = mulled_name.split(':')
else:
packages_hash, versions_hash = mulled_name, ''
print(f'Image Name : {packages_hash}')
print(f'Version/Tag Name : {versions_hash}')
print(f'Quay.io URL : https://quay.io/repository/biocontainers/{packages_hash}')
print(f'Docker pull command (quess): quay.io/biocontainers/{mulled_name}-0')
print()
if __name__ == '__main__':
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment