├── requirements.txt ├── nixml ├── __init__.py ├── nixml_version.py ├── languages │ ├── nix.py │ ├── __init__.py │ ├── texlive.py │ └── python.py ├── generate.py ├── snapshots.py ├── main.py └── data │ └── snapshots.tsv ├── tests ├── python36-smoke │ ├── run.sh │ ├── versions.py │ ├── expected.stdout.txt │ └── env.nml ├── .gitignore └── python2-smoke │ ├── run.sh │ ├── expected.stdout.txt │ ├── versions.py │ └── env.nml ├── .gitignore ├── MANIFEST.in ├── env.nml ├── ChangeLog ├── LICENSE ├── scripts └── update-nixml-hashes.py ├── run-tests.sh ├── README.md └── setup.py /requirements.txt: -------------------------------------------------------------------------------- 1 | PyYAML 2 | requests 3 | -------------------------------------------------------------------------------- /nixml/__init__.py: -------------------------------------------------------------------------------- 1 | from . import generate 2 | -------------------------------------------------------------------------------- /nixml/nixml_version.py: -------------------------------------------------------------------------------- 1 | __version__ = '0.3-pre' 2 | -------------------------------------------------------------------------------- /tests/python36-smoke/run.sh: -------------------------------------------------------------------------------- 1 | ../python2-smoke/run.sh -------------------------------------------------------------------------------- /tests/.gitignore: -------------------------------------------------------------------------------- 1 | output.stdout.txt 2 | output.stderr.txt 3 | -------------------------------------------------------------------------------- /tests/python36-smoke/versions.py: -------------------------------------------------------------------------------- 1 | ../python2-smoke/versions.py -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__/ 2 | build/ 3 | dist/ 4 | nixml.egg-info/ 5 | 6 | nixml.nix 7 | -------------------------------------------------------------------------------- /nixml/languages/nix.py: -------------------------------------------------------------------------------- 1 | def generate(p, _output, _options): 2 | return p['modules'] 3 | -------------------------------------------------------------------------------- /nixml/languages/__init__.py: -------------------------------------------------------------------------------- 1 | from . import python 2 | from . import texlive 3 | from . import nix 4 | -------------------------------------------------------------------------------- /tests/python2-smoke/run.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | exec nixml run --command "python versions.py" 4 | -------------------------------------------------------------------------------- /tests/python2-smoke/expected.stdout.txt: -------------------------------------------------------------------------------- 1 | 2.7.15 (default, Apr 29 2018, 23:18:59) 2 | [GCC 7.3.0] 3 | 1.4.2 4 | 0.19.2 5 | 1.15.1 6 | -------------------------------------------------------------------------------- /tests/python36-smoke/expected.stdout.txt: -------------------------------------------------------------------------------- 1 | 3.6.8 (default, Dec 24 2018, 03:01:30) 2 | [GCC 7.3.0] 3 | 1.4.2 4 | 0.19.2 5 | 1.15.1 6 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include nixml/*py 2 | include nixml/*/*py 3 | include nixml/data/snapshots.tsv 4 | include README.md 5 | include AUTHORS 6 | include ChangeLog 7 | include COPYING 8 | include INSTALL 9 | include *requirements.txt 10 | -------------------------------------------------------------------------------- /tests/python2-smoke/versions.py: -------------------------------------------------------------------------------- 1 | import sys 2 | print(sys.version) 3 | 4 | import mahotas as mh 5 | print(mh.__version__) 6 | 7 | import sklearn 8 | print(sklearn.__version__) 9 | 10 | import numpy as np 11 | print(np.__version__) 12 | 13 | -------------------------------------------------------------------------------- /env.nml: -------------------------------------------------------------------------------- 1 | nixml: v0.0 2 | snapshot: stable-19.04 3 | packages: 4 | - lang: python 5 | version: 3.6 6 | modules: 7 | - numpy 8 | - scipy 9 | - matplotlib 10 | - mahotas 11 | - jug 12 | - jupyter 13 | - scikitlearn 14 | - lang: nix 15 | modules: 16 | - vim 17 | -------------------------------------------------------------------------------- /tests/python2-smoke/env.nml: -------------------------------------------------------------------------------- 1 | nixml: v0.0 2 | snapshot: stable-19.03 3 | packages: 4 | - lang: python 5 | version: 2 6 | modules: 7 | - numpy 8 | - scipy 9 | - matplotlib 10 | - mahotas 11 | - jupyter 12 | - scikitlearn 13 | - lang: nix 14 | modules: 15 | - vim 16 | -------------------------------------------------------------------------------- /ChangeLog: -------------------------------------------------------------------------------- 1 | Version 0.2+ 2 | * Add package synonyms in Python 3 | 4 | Version 0.2 2020-03-14 by luispedro 5 | * Better error messages 6 | * Decouple snapshots from nixml releases 7 | * Update snapshots 8 | * Add requirements to setup.py 9 | 10 | Version 0.1 2019-03-26 by luispedro 11 | * First release 12 | 13 | 14 | -------------------------------------------------------------------------------- /tests/python36-smoke/env.nml: -------------------------------------------------------------------------------- 1 | nixml: v0.0 2 | snapshot: stable-19.03 3 | packages: 4 | - lang: python 5 | version: 3.6 6 | modules: 7 | - numpy 8 | - scipy 9 | - matplotlib 10 | - mahotas 11 | - jupyter 12 | - scikitlearn 13 | - lang: nix 14 | modules: 15 | - vim 16 | -------------------------------------------------------------------------------- /nixml/languages/texlive.py: -------------------------------------------------------------------------------- 1 | tex_group = ''' 2 | let 3 | tex = pkgs.texlive.combine {{ 4 | inherit (pkgs.texlive) 5 | {packages}; 6 | }}; 7 | in 8 | ''' 9 | 10 | def generate(data, output, _): 11 | ''' 12 | Generate a TeXLive group 13 | ''' 14 | packages = '\n '.join(data['modules']) 15 | output.write(tex_group.format(packages=packages)) 16 | return ['tex'] 17 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Luis Pedro Coelho 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /scripts/update-nixml-hashes.py: -------------------------------------------------------------------------------- 1 | try: 2 | from git import Repo 3 | except ImportError: 4 | print("import git failed") 5 | print("GiPython is needed: https://gitpython.readthedocs.io/") 6 | print("\t\tpip install GitPython") 7 | raise 8 | 9 | from datetime import datetime 10 | from sys import argv, exit 11 | import subprocess 12 | 13 | if len(argv) < 3: 14 | print(f"Usage: {argv[0]} ") 15 | exit(1) 16 | y2 = int(argv[1]) 17 | month = int(argv[2]) 18 | t = datetime(2000 + y2, month, 1).timestamp() 19 | 20 | 21 | # We only check commits in the last year 22 | min_t = datetime(2000 + y2 - 1, month , 1).timestamp() 23 | r = Repo('.') 24 | 25 | def iter_after(c, min_t): 26 | seen = set() 27 | stack = [c] 28 | while stack: 29 | c = stack.pop() 30 | if c.hexsha in seen: 31 | continue 32 | if c.authored_date < min_t: 33 | continue 34 | yield c 35 | seen.add(c.hexsha) 36 | stack.extend(c.parents) 37 | 38 | for nixml_name, branch_name in [ 39 | ('unstable', 'master'), 40 | ('stable', 'nixos-23.05'), 41 | ]: 42 | [head] = [b for b in r.branches if b.name == branch_name] 43 | candidates = [c for c in iter_after(head.commit, min_t) if c.authored_date >= t] 44 | candidates.sort(key=lambda c : c.authored_date) 45 | h = candidates[0].hexsha 46 | s = subprocess.check_output(['nix-prefetch-url', '--type', 'sha256', '--unpack', f'https://github.com/nixos/nixpkgs/archive/{h}.tar.gz']) 47 | s = s.decode('ascii').strip() 48 | print(f'{nixml_name}-{y2}.{month:02}\t{h}\t{s}') 49 | -------------------------------------------------------------------------------- /nixml/languages/python.py: -------------------------------------------------------------------------------- 1 | python_group = ''' 2 | let 3 | pwp = python{py_version}.buildEnv.override {{ 4 | extraLibs = (with python{py_version}Packages; [ 5 | {packages} 6 | ]); 7 | ignoreCollisions = true; 8 | }}; 9 | 10 | in 11 | ''' 12 | 13 | synonyms = { 14 | 'keras': ('Keras', 'package names are case sensitive'), 15 | 16 | # Be nice to users 17 | 'sklearn': ('scikitlearn', None), 18 | 'scikit-learn': ('scikitlearn', None), 19 | } 20 | 21 | def generate(data, output, _): 22 | ''' 23 | Generate a Python group 24 | ''' 25 | py_version = str(data['version']) 26 | py_version = { 27 | '2.7': '2', 28 | '3': '36', 29 | '3.4': '34', 30 | '3.5': '35', 31 | '3.6': '36', 32 | '3.7': '37', 33 | '3.8': '38', 34 | '3.9': '39', 35 | '3.10': '310', 36 | '3.11': '311', 37 | '3.12': '312', 38 | }.get(py_version, py_version) 39 | packages = [] 40 | for p in data['modules']: 41 | if p in synonyms: 42 | np,note = synonyms[p] 43 | if note is not None: 44 | message = "Replacing package '{}' by '{}' ({}).".format(p, np, note) 45 | else: 46 | message = "Replacing package '{}' by its canonical name '{}'.".format(p, np) 47 | print(message) 48 | packages.append(np) 49 | else: 50 | packages.append(p) 51 | packages = '\n '.join(packages) 52 | output.write(python_group.format(py_version=py_version, packages=packages)) 53 | return ['pwp'] 54 | -------------------------------------------------------------------------------- /nixml/generate.py: -------------------------------------------------------------------------------- 1 | from . import languages, snapshots 2 | 3 | import_nixpkgs_nocache = ''' 4 | with (import (builtins.fetchTarball {{ 5 | name = "nixml-{name}"; 6 | url = https://github.com/nixos/nixpkgs/archive/{rev}.tar.gz; 7 | sha256 = "{sha256}"; 8 | }}) {{}}); 9 | ''' 10 | 11 | import_nixpkgs_cache = ''' 12 | with (import (builtins.fetchGit {{ 13 | name = "nixml-{name}"; 14 | url = https://github.com/nixos/nixpkgs/; 15 | rev = "{rev}"; 16 | }}) {{}}); 17 | ''' 18 | 19 | mkDerivation = ''' 20 | stdenv.mkDerivation {{ 21 | name = "{name}"; 22 | buildInputs = [ 23 | {buildInputs} 24 | ]; 25 | }} 26 | ''' 27 | 28 | def write_nix(data, output, options): 29 | ''' 30 | Write .nix encoding of the 31 | ''' 32 | nixml_version = data['nixml'] 33 | if nixml_version != 'v0.0': 34 | raise NotImplementedError('Only nixml v0.0 is currently supported') 35 | sn = snapshots.get_snapshot(data['snapshot']) 36 | import_nixpkgs = (import_nixpkgs_cache 37 | if options.cache_git 38 | else import_nixpkgs_nocache) 39 | output.write(import_nixpkgs.format(name=data['snapshot'], rev=sn.rev, sha256=sn.sha256)) 40 | buildInputs = [] 41 | for p in data['packages']: 42 | langmod = { 43 | 'python': languages.python, 44 | 'texlive': languages.texlive, 45 | 'nix': languages.nix, 46 | }.get(p['lang']) 47 | if langmod is None: 48 | raise NotImplementedError("Unsupported lang '{}' (currently supported: 'python', 'texlive', 'nix')".format(p['lang'])) 49 | else: 50 | buildInputs.extend(langmod.generate(p, output, options)) 51 | name = p.get('name', 'pynix-env') 52 | output.write(mkDerivation.format(name=name, buildInputs=' '.join(buildInputs))) 53 | -------------------------------------------------------------------------------- /run-tests.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | shopt -s nullglob 4 | 5 | # This script is located on the root of the repository: 6 | basedir="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" 7 | 8 | ok="yes" 9 | for testdir in tests/*; do 10 | if test -d "$testdir"; then 11 | cur_ok=yes 12 | if test -f "${testdir}/TRAVIS_SKIP" -a "x$TRAVIS" = xtrue; then 13 | echo "Skipping $testdir on Travis" 14 | continue 15 | fi 16 | echo "Running $testdir" 17 | cd "$testdir" 18 | ./run.sh > output.stdout.txt 2>output.stderr.txt 19 | run_exit=$? 20 | if test $run_exit -ne "0"; then 21 | echo "Error non-zero exit in test: $testdir" 22 | cur_ok=no 23 | fi 24 | for f in expected.*; do 25 | out=output${f#expected} 26 | diff -u "$f" "$out" 27 | if test $? -ne "0"; then 28 | echo "ERROR in test $testdir: $out did not match $f" 29 | cur_ok=no 30 | fi 31 | done 32 | if test -x ./check.sh; then 33 | ./check.sh 34 | if test $? -ne "0"; then 35 | echo "ERROR in test $testdir: ./check.sh failed" 36 | cur_ok=no 37 | fi 38 | fi 39 | 40 | if test $cur_ok = "no"; then 41 | echo "STDOUT was" 42 | cat output.stdout.txt 43 | echo "STDERR was" 44 | cat output.stderr.txt 45 | ok=no 46 | fi 47 | 48 | if test -x ./cleanup.sh; then 49 | ./cleanup.sh 50 | fi 51 | rm -rf temp 52 | rm -f output.* 53 | cd "$basedir" 54 | fi 55 | done 56 | 57 | if test $ok = "yes"; then 58 | echo "All done." 59 | else 60 | echo "An error occurred." 61 | exit 1 62 | fi 63 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # NIX-ML : Easy, reproducible, environments with Nix + YAML 2 | 3 | Simple, perfectly reproducible, environments with [nix](https://nixos.org) 4 | specified using an YAML file. 5 | 6 | Example, write to a file called `env.nml`: 7 | 8 | nixml: v0.0 9 | snapshot: stable-20.03 10 | packages: 11 | - lang: python 12 | version: 2 13 | modules: 14 | - numpy 15 | - scipy 16 | - matplotlib 17 | - mahotas 18 | - jupyter 19 | - scikitlearn 20 | 21 | Now, run 22 | 23 | ```bash 24 | nixml shell 25 | ``` 26 | 27 | and you will be dropped into an environment containing the packages listed 28 | above, as was up to date in March 2020. Conceptually, the environment will 29 | always be generated from scratch, but caching means that the first time will 30 | take significantly longer (including, it will download all dependencies). 31 | Afterwards, it should take a few seconds at most. 32 | 33 | This environment will be like a typical _conda/pip/virtualenv/..._ environment: 34 | if will place the corresponding binaries at the front of the `PATH` so that 35 | they are picked with high priority, but, alternatively, you can generate a 36 | _pure environment_, which will contain **only the packages that you specify**. 37 | This avoids accidental use of packages that are not part of the environment: 38 | 39 | ```bash 40 | nixml shell --pure 41 | ``` 42 | 43 | Finally, you can run 44 | 45 | ```bash 46 | nixml generate 47 | ``` 48 | 49 | to just create the `nixml.nix` corresponding to the enviroment. 50 | 51 | ## Dependencies 52 | 53 | - Python 54 | - [nix](https://nixos.org) 55 | 56 | ## NIXML Format 57 | 58 | It's a YaML file 59 | 60 | `nixml`: version of nixml to use. Currently, only `v0.0` is supported. 61 | 62 | `snapshot`: The general syntax is `{stable,unstable}-{year}.{month}`. Snapshots 63 | since 19.03 are available. 64 | 65 | `packages`: A list of packages, which are grouped into language blocks. 66 | Currently supported: 67 | 68 | - `python`: Python language environment, specify the version (`version`) and `modules`. 69 | - `texlive`: Texlive packages 70 | - `nix`: Generic packages (i.e., `vim` or `bash`) 71 | 72 | ## Author 73 | 74 | - [Luis Pedro Coelho](http://luispedro.org) (email: 75 | [luis@luispedro.org](mailto:luis@luispedro.org) on twitter: 76 | [@luispedrocoelho](https://twitter.com/luispedrocoelho)) 77 | 78 | -------------------------------------------------------------------------------- /nixml/snapshots.py: -------------------------------------------------------------------------------- 1 | from collections import namedtuple 2 | Snapshot = namedtuple('Snapshot', ['rev', 'sha256']) 3 | 4 | GITHUB_URL = 'https://raw.githubusercontent.com/luispedro/nixml/master/nixml/data/snapshots.tsv' 5 | 6 | def get_snapshot(snapshot): 7 | '''Retrieve snapshot''' 8 | # Check in order 9 | # 1. Data shipped with the package 10 | # 2. Data previously downloaded 11 | # 3. Data updated from github 12 | 13 | from os import path 14 | import re 15 | if not re.match(r'^(stable|unstable)-\d{2}\.\d{2}$', snapshot): 16 | raise ValueError("Illegal snapshot value '{}'".format(snapshot)) 17 | 18 | try: 19 | data_paths = [path.join( 20 | path.dirname(path.abspath(__file__)), 21 | 'data', 22 | 'snapshots.tsv')] 23 | except NameError: 24 | data_paths = [] 25 | data_paths.append(snapshot_cache(True)) 26 | for p in data_paths: 27 | if p is None: 28 | continue 29 | snaps = load_snapshot_data_from(p) 30 | r = snaps.get(snapshot) 31 | if r: 32 | return r 33 | print("Snapshot '{}' not found locally. Updating snapshot data...".format(snapshot)) 34 | snaps = load_snapshot_data_from(update_snapshot_data()) 35 | r = snaps.get(snapshot) 36 | if r: 37 | return r 38 | raise ValueError("Snapshot '{}' not found".format(snapshot)) 39 | 40 | 41 | def load_snapshot_data_from(data_file): 42 | snapshots = {} 43 | for line in open(data_file): 44 | name, rev, sha256 = line.strip().split('\t') 45 | snapshots[name] = Snapshot(rev=rev, sha256=sha256) 46 | return snapshots 47 | 48 | def snapshot_cache(check_exists): 49 | '''Returns location of snapshot file''' 50 | from os import path, makedirs 51 | cache_directory = path.expanduser("~/.cache/nixml/") 52 | cache_file = cache_directory + "snapshots.tsv" 53 | if check_exists and not path.exists(cache_file): 54 | return None 55 | makedirs(cache_directory, exist_ok=True) 56 | return cache_file 57 | 58 | def update_snapshot_data(): 59 | '''Downloads snapshot data from Github 60 | 61 | Returns the path to the downloaded file''' 62 | import requests 63 | r = requests.get(GITHUB_URL) 64 | ofile = snapshot_cache(False) 65 | with open(ofile, 'wt') as output: 66 | output.write(r.text) 67 | return ofile 68 | 69 | 70 | -------------------------------------------------------------------------------- /nixml/main.py: -------------------------------------------------------------------------------- 1 | from . import generate 2 | from sys import exit 3 | 4 | try: 5 | import argparse 6 | except ImportError: 7 | print("argparse not found. Please install argparse with 'pip install argparse'") 8 | exit(1) 9 | 10 | def main(args=None): 11 | if args is None: 12 | from sys import argv 13 | args = argv[1:] 14 | import yaml 15 | parser = argparse.ArgumentParser() 16 | parser.add_argument("-c", "--cache-git", action="store_true", 17 | help="Cache git download: will be slow the first time, but will make it faster to switch between different snapshots") 18 | 19 | sp = parser.add_subparsers() 20 | 21 | parser_generate = sp.add_parser('generate', help='Generate .nix file') 22 | parser_generate.set_defaults(sub='generate') 23 | 24 | parser_shell = sp.add_parser('shell', help='Create a shell') 25 | parser_shell.set_defaults(sub='shell') 26 | parser_shell.add_argument('--pure', action='store_true', 27 | help='Run a pure shell, isolated from the rest of your system') 28 | 29 | parser_run = sp.add_parser('run', help='Run a command in the shell') 30 | parser_run.add_argument('--command', action='store') 31 | parser_run.add_argument('--pure', action='store_true', 32 | help='Run a pure shell, isolated from the rest of your system') 33 | parser_run.set_defaults(sub='run') 34 | 35 | opts = parser.parse_args(args) 36 | 37 | with open('env.nml') as ifile: 38 | data = yaml.safe_load(ifile) 39 | 40 | actions = { 41 | 'shell': ['generate', 'shell'], 42 | 'generate': ['generate'], 43 | 'run': ['generate', 'run'], 44 | 'no-sub': ['error-no-sub'] 45 | }.get(getattr(opts, 'sub', 'no-sub')) 46 | 47 | if actions is None: 48 | from sys import stderr 49 | stderr.write("Unknown subcommand '{}'\n".format(opts.sub)) 50 | exit(1) 51 | if 'error-no-sub' in actions: 52 | from sys import stderr 53 | stderr.write("You must use a subcommand as argument to nixml.\n\nUse --help for a list of subcommands.\n") 54 | exit(1) 55 | 56 | 57 | nix_file = 'nixml.nix' 58 | if 'generate' in actions: 59 | with open(nix_file, 'w') as output: 60 | generate.write_nix(data, output, opts) 61 | if 'shell' in actions or 'run' in actions: 62 | import subprocess 63 | nix_shell_args = ['nix-shell', nix_file] 64 | if opts.pure: 65 | nix_shell_args.append('--pure') 66 | if 'run' in actions: 67 | nix_shell_args.append('--command') 68 | nix_shell_args.append(opts.command) 69 | exit(subprocess.call(nix_shell_args)) 70 | 71 | if __name__ == '__main__': 72 | from sys import argv 73 | main(argv[1:]) 74 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2019-2020, Luis Pedro Coelho 2 | # vim: set ts=4 sts=4 sw=4 expandtab smartindent: 3 | # 4 | # Permission is hereby granted, free of charge, to any person obtaining a copy 5 | # of this software and associated documentation files (the "Software"), to deal 6 | # in the Software without restriction, including without limitation the rights 7 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | # copies of the Software, and to permit persons to whom the Software is 9 | # furnished to do so, subject to the following conditions: 10 | # 11 | # The above copyright notice and this permission notice shall be included in 12 | # all copies or substantial portions of the Software. 13 | # 14 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | # THE SOFTWARE. 21 | 22 | try: 23 | import setuptools 24 | except: 25 | print(''' 26 | setuptools not found. 27 | 28 | On linux, the package is often called python-setuptools''') 29 | from sys import exit 30 | exit(1) 31 | import os 32 | 33 | exec(compile(open('nixml/nixml_version.py').read(), 34 | 'nixml/nixml_version.py', 'exec')) 35 | 36 | try: 37 | long_description = open('README.md', encoding='utf-8').read() 38 | except: 39 | long_description = open('README.md').read() 40 | 41 | install_requires = open('requirements.txt').read() 42 | 43 | packages = setuptools.find_packages() 44 | package_dir = { 45 | 'nixml': 'nixml/', 46 | } 47 | package_data = { 48 | 'nixml': ['data/snapshots.tsv'], 49 | } 50 | 51 | classifiers = [ 52 | 'Intended Audience :: Developers', 53 | 'Programming Language :: Python', 54 | 'Programming Language :: Python :: 2', 55 | 'Programming Language :: Python :: 2.7', 56 | 'Programming Language :: Python :: 3', 57 | 'Programming Language :: Python :: 3.3', 58 | 'Programming Language :: Python :: 3.4', 59 | 'Programming Language :: Python :: 3.5', 60 | 'Programming Language :: Python :: 3.6', 61 | 'Operating System :: OS Independent', 62 | 'License :: OSI Approved :: MIT License', 63 | ] 64 | 65 | setuptools.setup(name = 'nixml', 66 | version = __version__, 67 | description = 'NixML: reproducible environments with nix + YAML', 68 | long_description = long_description, 69 | long_description_content_type = 'text/markdown', 70 | author = 'Luis Pedro Coelho', 71 | author_email = 'luis@luispedro.org', 72 | license = 'MIT', 73 | platforms = ['Any'], 74 | classifiers = classifiers, 75 | package_dir = package_dir, 76 | package_data = package_data, 77 | packages = packages, 78 | install_requires = install_requires, 79 | entry_points={ 80 | 'console_scripts': [ 81 | 'nixml = nixml.main:main', 82 | ], 83 | }, 84 | ) 85 | 86 | -------------------------------------------------------------------------------- /nixml/data/snapshots.tsv: -------------------------------------------------------------------------------- 1 | Snapshot Revision sha256 2 | stable-23.10 3b674351c22d1e5bf9e0f7ef3bf4d2c6bde46d2d 0c3rqxfwfrfpd3vlhrcwv471q1h445zzvj6qncm4jnzrxrkwpqnv 3 | stable-23.09 df58c47c601d008ebf24fa45b6f0c408a8bf95c1 1axjz58mpvlz8ss2mbmpafzb36c1p0wdqx8a7b0n0v6id4fz30qx 4 | stable-23.08 fa10e0f4b62fd985855d8040b0c928291afc836e 0xj1cpfnbxivwp78bn83ksivv6ajqk2s2j5a5hjfmh2v4sb1s2by 5 | stable-23.07 115cbe461dc80d029b236e6d8d89086e43491aaf 010lklmsiarlghfa98ngz5g9a248h84fmq2vvcpg1dgrdism4v54 6 | stable-23.06 b33fe740d1960e651f7d43c2531f4db8d001859b 1pc1418a13l7q7a1gbf79fkv5sx2c4y7r7c21hi1nsqb1rm2bcx9 7 | stable-23.05 23ba6f3225fdf2d72cd0423af4169f60d7d7e7c0 1gi234m57jv5ajr8fzw1ry3pn4lq1fy3c2a78m91q07h1qpliiml 8 | stable-23.04 7c3e04ed3eaa8f21cc9ebc86f9c9b44c44a7fdb8 1jgmanzmgbn9lxp0zc55lj1ml09zrkkwb00axx2iii06mbbv6991 9 | stable-23.03 5048b87bac7ae573faa1afe114be1b8cd86637a5 173gxl2vgmk7jgldq2jm84fq65rwvjjjyx4c57z8yazfn06pscg0 10 | stable-23.02 a8ad45ed0a461dc64b0d51a8f09f12f31fb8d268 120jlr8xqdlisfc5cshs3nckp6xnirnlh7lvl4liy56s0ap09c85 11 | stable-23.01 18b12726227c3641af3494ebe9742bd58c406bf4 13ic4a7n719x5yjmd0138hdh2sggcrw4iwkfrq8ili0y5b602g6b 12 | stable-22.12 69ced099b29bfc33bd28fad0f18f3e8dbe939958 0n9d48pj4imffhywni8ylib79q6qzkqfzhwcb4xvg4ry7s07p065 13 | stable-22.11 30fb0d2927fb26dce87d484f3bb936d486d67d8c 0ykbqcfwx338m1jcln9pj629byxbyr448d88wsryp8sf6p611cv2 14 | stable-22.10 eb3e2e738b3bbd434d23053b77384895bb633d2e 02zj7caib69cghrmg39wxf42hrmgmj2zkxcyf9c8csdgr6imymq2 15 | stable-22.09 440b8d25c465b9cfec5792be4a334b5e30f0bd0c 1sbrw18db7cr2pzd8w8bxw8c4vh4qzqd80lfbiqkz9lhj892j20h 16 | stable-22.08 99e0953d656fb05056a83b54242da4253b4d37bd 0yscq4vp479kba4a74l535sqhswwv0h0n238ha8kmd9wff1d2x3f 17 | stable-22.07 e6ec584e25e791d6eadbcadc4610bc8d14deafbb 18y05lz3kjrh1ym02jdi4bhm8qyk39d2s5r995xml771yf26n5yw 18 | stable-22.06 272c9ec408135ab48fcc27de3d69a924b7940646 1sicihv8zffcz145rx5qx7zzdviga1mqr25mc052nrzd61g8jw3b 19 | stable-22.05 5c0c0f0ee8d7b536925580e723a5f03bf6e0b7a4 0v1fbc261by0dfdwysr906garc081swwlyy5x3d7daf98gr5k7yx 20 | stable-22.04 82906cbfb8969278e6edbea2d756c6b62a628276 1lm8s5h9hg375an3cq9i86f0ycby32ycfb000nc0qjz1770x2f3p 21 | stable-22.03 53a52dec8ea77787693a98983a79dfcc9ced749c 1c96h2a645rpkh03n50v0g67zbc9ks7hldrwnzjwssjcd5rqc6fq 22 | stable-22.02 588c378afebf1f06650c06d21d1c6a3c7a921629 0ikhx7rqv5h4da3swwrpv4hgg8id9nqvh6yi4c1cwppp1picz91z 23 | stable-22.01 351c348d8971a48c117b05fdf84fdbfdfb93898b 12j9l91hns1gz5bd6aznz8lv5rgc302mh7mk8pr7ma415gjpl9sm 24 | stable-21.12 aad23cbae54f1780d2e06bcec2b01a764c965d6c 0yizxr4jpnm1gcxa5kb58z130qc6wj4zycgn8nywacbhccd3g44x 25 | stable-21.11 8a30d1e59049053beb92a1043ecfa0069ca6575e 150rrksrjf6w9m3c1ll04xilpglysklfpi636rxwyy318g5xss55 26 | stable-21.10 02f5a85d1e9b62278c2f522233cfbc7e07e1aebc 0hgbkwkgnpzgr5lmpd5c04rykybh8ax75bnyhqy14bv21apxsmvz 27 | stable-21.09 b6598fc6c34f0a904b694f51ae3f3c225567d0dd 1zi0h8wjk3sk1ksd8yyg5qyx0hrpbg4mbxy0xlrvsc9fqwfzx7pg 28 | stable-21.08 63ecd177a7b4c52338655e4aef54dd6e5e4c4d0d 0jxg5bnskfi9xpnw2n8czwjkfcimm2h3174vpvgs84rscb5nrd4x 29 | stable-21.07 e9b8abb76b0689330f55e61402cab4d038f0a4e6 1z559bq3ja1bphhgs1kngjbgipkcmjs95cnj6ycfbdhvz4yh13nh 30 | stable-21.06 0c6243e00ff1a48b080145702ea6223446b29c1b 1514wwxqi4c4ryhwd2j0b3r2rph25ji4nczkq171la123fviq7gd 31 | stable-21.05 79b29fbde8358be601db25ef3106aa989148af2d 0c8c133qw7nw8ygpgh9w0wfzmw4wivkpmixj7s4jhl2zddihx5x7 32 | stable-21.04 a034097f9b2deab2f554a2e9c38e8633f4f9be23 1jzwfic8i0i310s19xi4vhizp4yl5w73g8ccnkpnxqw0k2i3rj6f 33 | stable-21.03 e7973f9408a95998d0249f377c8300860b057a27 0927g7h5xzzcyx41z4b6623j3qwn0v7dmisl7gpk819yag834wlc 34 | stable-21.02 804f750bdbdf0b8ef19e7bb219368fa26718be8e 138yq3nnqyxqfzjpic0z500l76ysmxwrx4linqszc0sa33qcni22 35 | stable-21.01 16fd4233de22166a25833bc4b130f537a8213ab4 1iiyl6y9cgs10pg2zi7b8gf9vhsj542vvqmhqamzbr3fgnbcl0g3 36 | stable-20.12 177f134f288b8b31af7e5ac7d91cf25bad0b05ef 0pd6pzn4r3rqb9id7dla57jpxrcgwkmfbmbr0gmg8kpki2g071fl 37 | stable-20.11 9ff426c22e2881937fcac67dbc5f447e10b37804 09kbk1f0isz75jcf7g7610b0rric81rbs4600g9h76fg5vmcb0qp 38 | stable-20.10 909ef02aa143087320c2751bf12c35396c331a01 0dvdmbr785320cqljcw7p1v32zdcnsd0jvj0cjrnv3byv4w8ibw0 39 | stable-20.09 0d60b0b10eae7a29abb1cbcd47a764c752b39bd9 067aj1j2zp919gqj8cq749x9byxclb732dxkh1z65bddkca76nnm 40 | stable-20.08 977000f149bd4ddb65bf1af09552898a0f305c72 1ad3bviywxd64g4vwk4ppjkkajd3rfqjhnp6xcz3w0ih0sbn0rh5 41 | stable-20.07 a1a8e7b0217fd6a72a5d008d8dfb3fdf8ba92e00 18xna03j2vp551f2x80w7y02865hvpmrppp5i6mxp2g36bldym6h 42 | stable-20.06 36641b25e75e8c87759a00f2ab157dec753ded15 10mq9bc8rcwy6v5y9pxpvfx8wqdjyl5vy8sbp3591zkwxr74ns7i 43 | stable-20.05 aef39c7bccda5b94aa3c5362ac67c0ce3c03d6b3 1fyik7x2fv4dgy3ggcjjdmm1pl3svd6x7fk7aav8jn3hvqa97zgq 44 | stable-20.04 d011e4749457af484adf2e90062c83a44ad072a4 1nvhya0v98agidgxv83sb7xyb559qagx9f6iqknpxhxsv09j2qsp 45 | stable-20.03 0252dfb2685af7a01c8b7ac1de511474642ab2b6 1zxkxxwix9hszcj697aaas5q97ps4jnfsgkqg9m0lnp4s1v8dv4f 46 | stable-20.02 d3d2de8b99be56c5a6ed18f46e280f8103124dd7 1chzbajrspw24csnv2c0h1h59pbp1k6939cllmd61382p9iw7rg8 47 | stable-20.01 cf17d0e03386b55c3be5c381fbefc44c2fb53be2 0pklal0hds2cwmd6f3p4v4aia9zbq3w4qr9ip7m18d23j228gd3g 48 | stable-19.12 22a18d41f708b9b6b3882ed91c4559ab5dd7c269 0syq8xybkzsfk6d0y4bvzgzbb8srn3kz7nz2mhkp68fdwf3c834i 49 | stable-19.11 879339018534bad1e11955cc7b51bf9e7a721cd4 07v4qb50b3bkhrj8g5aip6v2m7qm1rdmhblm1cchrw0zvyx77kvi 50 | stable-19.10 9aa95fd69f4f5c3e21d900557630e8dfa73442c1 10vrky5nywzqxxk84v3q7kllxh4hf9vkpgd3qndl5q6jn13pwciq 51 | stable-19.09 ece8a6766572f48c647947fdd35057a78ff6eaf2 0a9yfkdlng4bzdfhcp55clpd75w908pwbin3ad5lwpxcx4dvhqwz 52 | stable-19.08 c41e3c536a96d588cc83d35802ece6ba1ed0e360 0mqz8hya16zs9ilfzsdfnjyyzx07g2znln58yrizvwn02j6x8cqj 53 | stable-19.07 85f820d6e41a301a06a44cd72430cce0b3e4e5f5 0iwn8lrhdldgdgz5rg7k8h5wavxw5y73j453ji7z84z805falfwi 54 | stable-19.06 52210d67e84efd2c5e37c8367018412c85def335 1qlcpv3i925i89mlfdm2h1igwr944jx59rl00qwwsxzz91qccniy 55 | stable-19.05 d740b2ee8551da5c81670e8bce2cc43abd136aa6 153ng9ilk5559n1nb2la0pj1j17fyv65rj2yy5m09w9q66j1wrjc 56 | stable-19.04 37694c8cc0e9ecab60d06f1d9a2fd0073bcc5fa3 1ibjg6ln2y764wfg0yd0v055jm4xzhmxy5lfz63m6jr3y16jdmzb 57 | stable-19.03 c42f391c0c87429dafd059c2da2aff66edb00357 0yh8wmyws63lc757akgwclvjgl5hk763ci26ndz04dpw6frsrlkq 58 | unstable-23.10 efcde1d6685d05db10e3bce06563d225cba90c48 1jq2yy1777pnzgf1dqxcpv04xvg9jm35cfar99yhzw9y2sc7cn43 59 | unstable-23.09 f8c26c97e9f31a94e75f94c79e3574035cdc1e8a 04ilcb01485f6v73akyxk33lrv3l9zz3qzzb4rsn9vvrs1awrljl 60 | unstable-23.08 6d634ba3152f781f5efcc84be8a44a49d5d4305f 1izkpqk98ra3qms9csp60jysmmrk8v52vxc7sjxyqg81cfqhvxnb 61 | unstable-23.07 5ddf5325543c4b0b6a6cdfd6043f4a722dc470ca 1lx0dqhq7zh6wa3ya814pyl98ala97s2xix3ciby21y3sq322hwj 62 | unstable-23.06 c54049db5cd08f410a162be0eddc6fbdb751f2dc 05di9xkw2kw363xsz60s4nq4150agmq6dk4bljsyl9rw22g51m0b 63 | unstable-23.05 85d926ecb2d0932f6cb3d53aa9b2ef26c5608cda 0dda9l1hlsshqarpz2sk4r07vdagx84lqxsmwyxw74b1zgl0w9g0 64 | unstable-23.04 e48d50d23b4f9819827db21d5e8bb4446fc7dc91 1l6xlj452p8m1cgyyscpzr2hcifd62mbc55rwhz4nk874d1sspnc 65 | unstable-23.03 7c0fe0cb669d8cd9544d9ac04ecef7653267ddc7 1ccnnbmqg8da5zpxj5b5f44y93ia4rz710k4gipjv0qlgk1wa3wd 66 | unstable-23.02 27ca218d2fc098ff81a6a9d49dd70daa15d3659a 1hsygvrnnyy6kl8kk7sd499m1zank27xi8h9j4r8xqp4w1a7s5q8 67 | unstable-23.01 ab1c4783203df1937d72867b1fc3e269317ae7c8 06j4ny2nj863bi879d7ajl54r3b2gqn8cn97kfd2yzf01dxzqnz8 68 | unstable-22.12 3f09bb9d97e26eb8ba8a3b162fd0782dadff9b31 0m3ywijf7lxzpbg617nnp6pv2vb17ap8dwyj7l94p8rxcm5z6023 69 | unstable-22.11 facfd565093c8f410be840dc427975a6d7368f91 0wfyysxdhgkp3469j8ny5lrd9jrbc8y42fa3nsrg930y5kp9h06a 70 | unstable-22.10 e3d61133e45f9a20ca145c65e7c210071ee8e524 142l6a83i81dvm5zvxfr0xilcvsg4g7v2i8jb76np02ah29rp7gn 71 | unstable-22.09 3ba8ce573af292229589a9f14f5e8bfb01c23146 1n491qaf3gdfkh3pqv1rypm6klys04zsg7jif03ixvi9r761gacb 72 | unstable-22.08 82640adbf00f9ebb5da9c6c47d0b8d242755946e 1rzrzi4rl67c1rggb1phr879hnh2c6j1khhb744a5817215jykgg 73 | unstable-22.07 3b953b5a6973d3762b60f3f91ba7c233bfa4490c 001fs03d8rmsl4rjqaiwzwxpj80g7302rj7vq6dppwsqmq5vfnxa 74 | unstable-22.06 4845c937139789c40f7da7ff539ccc40eb62e653 0xk5myqvdnaahlrb1vgjfnwv625hkiam9mshppghdswjmqzrcg29 75 | unstable-22.05 67f45a43621047fead6bdd5c87509c51a88b9871 1h2rka79wjzv5nsndbqhfir44rk1lxa86ykdhsnrb0js8b7majm8 76 | unstable-22.04 1e2c1b83728c51a23f183e518c3a312f15dd0aaf 18js0cmqjd20s561nvnr8h4g3b411ydc2zd87kgzrq69jwwj1m12 77 | unstable-22.03 1f59c76c66a946075b83195c55bae8938c303d05 08dplq0mr7arww1qahi2jr1pzp9bp7ak5nb73vlvygnfahvf9lv1 78 | unstable-22.02 764e39c6c4702e8cf85b1740803ab48ce1aa3885 0983rbv35s0ylzq7dgj70gzzazkzahszw31j3i01q06wjb11yk06 79 | unstable-22.01 49a3e9995f2ac507ec017b1a17f050ab0629cf4e 1l9w9k5yay3pq2fh1hnw4vim474xh6nadfbgm00xqv8zivrrfk1i 80 | unstable-21.12 cb928788e024e2d2326f63550663acfd7205e759 0dr5ciz74lpvrjgvp3fsyq4xnlzrlqbkqiwmcl7w042gigkg5dxd 81 | unstable-21.11 315cf2c5dee8a35a39c41c71053750403064f201 12f61dslpf29aqlysvgg9alphigpygj0ryg0yfkdspsx60b9c1f8 82 | unstable-21.10 f78a19a86bea07ca910a0d01805dd2b920b9a438 17xfyxpc949169db6gkbh9kbiyfnc1b0jcxaccyybkb6ymsi10s5 83 | unstable-21.09 dc47a7bef8848c4e52440b45bc4c0c388b010a16 1yxha9jqmksjb82l6zsyfs7sqjvhlfvkp5c6gk28xm8m2s6kq51x 84 | unstable-21.08 cb7d80dcaf926a38e80044c29a8210e69f27bb80 0cicl2183nsa7zhk1xpcqslvavn414ls08h8rkym5vzagmfkmgw9 85 | unstable-21.07 e8d4f67e8af80a7176385a4f2f6d726afe64f093 1r3hcd709zdbddfv6agvqc1n7yibd0c1p8z2094jbdv3gch3yg1g 86 | unstable-21.06 0c2c7038919a0e4a5802ab04a12302d4c73b13b2 0jkfpwj1ygccffzwdskmwx2ckzjcg0l2b00pgpvm17lqw4gmbi3m 87 | unstable-21.05 5c0c0f0ee8d7b536925580e723a5f03bf6e0b7a4 0v1fbc261by0dfdwysr906garc081swwlyy5x3d7daf98gr5k7yx 88 | unstable-21.04 6cb1865eb35f6d69c1eae661f41c713736f755d7 0iyby5zcf6c33d68rbqxad0fvq0kxpijd61362p56wj0rrl64kbr 89 | unstable-21.03 58aa8e36e2c20a4a49beb873afd82ec056d2347b 0c187pkkyak09c6aa1kh53cmwscsil6d6cfj7idpmfrcfy7fh84l 90 | unstable-21.02 4b9908322c02b8127aa6944a364d0f00656f0c9f 174gqylar47z29ya4isgzk66frmslfzlzb4xdy2s481m34cq8xfh 91 | unstable-21.01 5ab6b50b6016bf6231c4f7b6aa5f9c21f1cddd75 1zpx5i55z7lqb5fkknk6vn2fdv1vd7404ngx4zhgnbh7c18qif97 92 | unstable-20.12 e1e750a52ce2434ac098227d073ba3d8336d288e 1gmfh8cxrfh7ygasajikkxjfzcm66a1m52gnw9qaks0gdccj1ksx 93 | unstable-20.11 a70df2c2ccf5317f1ecbf5f1716364947b5b96b4 00sh5fw3xa52vifyc5w32zdbs54bspv474a4pipj21gwi0wrz5d3 94 | unstable-20.10 af27072f1d35dcde78ab1e3b384082f5498725e6 1fnldsqj6hn8qgbmwpv9qf56sn83fdqwkc2g2dhkv02k4qa94nn2 95 | unstable-20.10 af27072f1d35dcde78ab1e3b384082f5498725e6 1fnldsqj6hn8qgbmwpv9qf56sn83fdqwkc2g2dhkv02k4qa94nn2 96 | unstable-20.09 abfafe8681606835da53f334a99c4461531ff132 152p8935mv3pnp97np5v39m815293wa6f0a34517hd0q8jjv3365 97 | unstable-20.08 c06eab791fc7e39a36b706f85d17f471c54c2581 0hlj1aqqwa83f0ydxg3dpm7n3wgbxvqf52xdw022qw7bf6xwwq7h 98 | unstable-20.07 c684398c6afaf90d9dc86466ca36e4ea3263d77f 01v8m9j6jc0nbamrd25zwfxcc3xfhfha7cg7d8b3pslzsadghsia 99 | unstable-20.06 09244cbd98d119fdc9b8ead9dfe66b7f1b833ba4 1bxswrkif0060ivj7qzabvk23j249rva740c3b0gcknl20g0xsj0 100 | unstable-20.05 8aee1600f6684521046086dddc3e6a8c1461abe6 1w6f1x74m6klyv3qspxaxsjdda0x9a29vrdhwaam9xj84b5pgc2r 101 | unstable-20.04 68a50aaaa52db6e8f1efb801da4f361486ae1904 1kmf7vv9brmyw52q1fc7jk6dvld2hwqa8dc7d95rja1mmkszy6ff 102 | unstable-20.03 ba5b5dc17bbd0fc6e2dd2d943a9d79dd00fe1afe 1v6g91id0380fd0c03558fagmywwqnrpmidx81i73wvp38kndy92 103 | unstable-20.02 71db54a01f461dddd80ae9af106101221207eb98 17dm8dzk3nzmi5vk11kjaj64qf4cxvn2ccv3qh6birr3bnpkxmam 104 | unstable-20.01 b7637464049677e180a4f97aca353937861e6311 0x1zqsx49rmggsc5wzkiql98xzb42if1g4bdvlk0x4l6clxpfab9 105 | unstable-19.12 5171763b9ab6d253273e27a963d93d6c1c3004a0 1caawd545z8rp5z4d1hlf5sy9lzqhlnycf0g9nhkl3956wniy9la 106 | unstable-19.11 52adcff30eb2a11976864b726fa4d6681001833a 1l54mxlidh0527ccmji9l6v1ma1c0b66y2fbdvja9s6y0mmawd7x 107 | unstable-19.10 9cd1e90e00bd7fd05d22d50ac1649ed4c45df4d0 1xwsy32i0ivq97nayc04p96gv6fd3yhv0yp47jiicb88q60m2i0j 108 | unstable-19.09 96e5474329a3c2b2309872fb01b007daaf6d7bdc 0i00ldmacvj7apjw6hzpnnxn9d8n4v7dh7wr9l3ihxw9rr8vaqcw 109 | unstable-19.08 06183c2a27e4374eb5f1a7a7abbb1bd96a63e156 18ayw89iv0sxw5kk6af30jyf56wv8lk40ilcwbp45rkacqyxgpdg 110 | unstable-19.07 8b3dc7a3a0633aaa293d994921be925d6d703334 1gadsn02lj52mxy9q2nnhvbv3j566jkb27rbqq8p6nwg3gp02v8d 111 | unstable-19.06 60ede00cfcdc4330fbaf9f8e356fca2ec1496f70 1r0xk9pwbkr1bq238816l1k0gbkh5xxkdbppi0jgdbc3zd1h56dn 112 | unstable-19.05 f82bfd5e80f3798421b7d4a79984729d2bded0e1 1x7znrmy5y5yrzskbdirljax5liknibzzpr4daiqb4gj0fv2g1w2 113 | unstable-19.04 54bb7ed9270a8b16b2dd56fd52cbf31562b2bf4a 0qwhddz0vl8jib8imc8l9m5cddxpycvc5qly4gniy0iqvfiyx84j 114 | unstable-19.03 d64d42f12d253d4b0fde2b63e14d1f7d322b754c 1l8i523paqmhzgcn1v8z5jssry5ww46qnfrhj7832drgh1h7bxdx 115 | --------------------------------------------------------------------------------