diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 94156ba9..8ca24603 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -23,12 +23,24 @@ defaults: shell: bash jobs: package: - runs-on: ${{ matrix.os }} + name: ${{ matrix.os }}, Python ${{ matrix.pyver }}, ${{ matrix.micromamba && 'micromamba' || 'conda-standalone' }} + runs-on: ${{ matrix.os }}-latest strategy: fail-fast: false matrix: - os: [macos-latest, ubuntu-latest, windows-latest] + os: [macos, ubuntu, windows] pyver: ["3.7", "3.8", "3.9", "3.10"] + include: + - os: ubuntu + pyver: "3.9" + micromamba: true + - os: macos + pyver: "3.10" + micromamba: true + # Re-enable once micromamba supports menu creation + # - os: windows + # pyver: "3.8" + # micromamba: true env: PYTHONUNBUFFERED: True steps: @@ -112,12 +124,28 @@ jobs: :: Careful with the trailing spaces before the >> redirect! echo CONSTRUCTOR_PFX_CERTIFICATE_PASSWORD=1234>> %GITHUB_ENV% echo CONSTRUCTOR_SIGNTOOL_PATH=C:\Program Files (x86)\Windows Kits\10\bin\10.0.17763.0\x86\signtool.exe>> %GITHUB_ENV% + - name: Set up conda executable + run: | + source $CONDA/etc/profile.d/conda.sh + if [[ "${{ matrix.micromamba }}" != "" ]]; then + conda create -yqp ./micromamba -c conda-forge micromamba + if [[ ${{ matrix.os }} == "windows" ]]; then + echo "CONSTRUCTOR_CONDA_EXE=./micromamba/Library/bin/micromamba.exe" >> $GITHUB_ENV + else + echo "CONSTRUCTOR_CONDA_EXE=./micromamba/bin/micromamba" >> $GITHUB_ENV + fi + else + conda activate constructor + echo "CONSTRUCTOR_CONDA_EXE=$CONDA_PREFIX/standalone_conda/conda.exe" >> $GITHUB_ENV + fi - name: Run examples and prepare artifacts run: | source $CONDA/etc/profile.d/conda.sh conda activate constructor mkdir -p examples_artifacts/ - python scripts/run_examples.py --keep-artifacts=examples_artifacts/ + python scripts/run_examples.py \ + --keep-artifacts=examples_artifacts/ \ + --conda-exe="${CONSTRUCTOR_CONDA_EXE}" - name: Test with conda-libmamba-solver run: | source $CONDA/etc/profile.d/conda.sh diff --git a/CONSTRUCT.md b/CONSTRUCT.md index f23e7cd2..fb09991a 100644 --- a/CONSTRUCT.md +++ b/CONSTRUCT.md @@ -100,8 +100,8 @@ is contained as a result of resolving the specs for `python 2.7`. _required:_ no
_type:_ list
-A list of packages with menu items to be instsalled. The packages must have -necessary metadata in "Menu/.json"). Menu items are currently +A list of packages with menu items to be installed. The packages must have +necessary metadata in `Menu/.json`). Menu items are currently only supported on Windows. By default, all menu items will be installed; supplying this list allows a subset to be selected instead. diff --git a/constructor/construct.py b/constructor/construct.py index 3514f0b4..4e2502b8 100644 --- a/constructor/construct.py +++ b/constructor/construct.py @@ -79,9 +79,9 @@ is contained as a result of resolving the specs for `python 2.7`. '''), - ('menu_packages', False, list, ''' -A list of packages with menu items to be instsalled. The packages must have -necessary metadata in "Menu/.json"). Menu items are currently + ('menu_packages', False, list, ''' +A list of packages with menu items to be installed. The packages must have +necessary metadata in `Menu/.json`). Menu items are currently only supported on Windows. By default, all menu items will be installed; supplying this list allows a subset to be selected instead. '''), diff --git a/constructor/main.py b/constructor/main.py index 44e680da..3eff871b 100644 --- a/constructor/main.py +++ b/constructor/main.py @@ -86,6 +86,9 @@ def main_build(dir_path, output_dir='.', platform=cc_platform, if platform != cc_platform and 'pkg' in itypes and not cc_platform.startswith('osx-'): sys.exit("Error: cannot construct a macOS 'pkg' installer on '%s'" % cc_platform) + if osname == "win" and "micromamba" in os.path.basename(info['_conda_exe']): + # TODO: Remove when shortcut creation is implemented on micromamba + sys.exit("Error: micromamba is not supported on Windows installers.") if verbose: print('conda packages download: %s' % info['_download_dir']) @@ -293,7 +296,7 @@ def main(): version='%(prog)s {version}'.format(version=__version__)) p.add_argument('--conda-exe', - help="path to conda executable", + help="path to conda executable (conda-standalone, micromamba)", action="store", metavar="CONDA_EXE") diff --git a/constructor/preconda.py b/constructor/preconda.py index f3e8935c..5155a405 100644 --- a/constructor/preconda.py +++ b/constructor/preconda.py @@ -20,7 +20,7 @@ from .conda_interface import (CONDA_INTERFACE_VERSION, Dist, MatchSpec, default_prefix, PrefixData, write_repodata, get_repodata, all_channel_urls) from .conda_interface import distro as conda_distro -from .utils import get_final_channels +from .utils import get_final_channels, ensure_transmuted_ext try: import json @@ -127,6 +127,7 @@ def write_files(info, dst_dir): with open(join(dst_dir, 'urls'), 'w') as fo: for url, md5 in all_final_urls_md5s: + url = ensure_transmuted_ext(info, url) fo.write('%s#%s\n' % (url, md5)) with open(join(dst_dir, 'urls.txt'), 'w') as fo: diff --git a/constructor/utils.py b/constructor/utils.py index 3cd65a64..4d48c3c9 100644 --- a/constructor/utils.py +++ b/constructor/utils.py @@ -8,7 +8,7 @@ import sys import hashlib import math -from os.path import normpath, islink, isfile, isdir +from os.path import normpath, islink, isfile, isdir, basename from os import sep, unlink from shutil import rmtree @@ -123,6 +123,24 @@ def add_condarc(info): yield 'EOF' +def ensure_transmuted_ext(info, url): + """ + If transmuting, micromamba won't find the dist in the preconda tarball + unless it has the (correct and transmuted) extension. Otherwise, the command + `micromamba constructor --extract-tarballs` fails. + Unfortunately this means the `urls` file might end up containing + fake URLs, since those .conda archives might not really exist online, + and they were only created locally. + """ + if ( + info.get("transmute_file_type") == ".conda" + and "micromamba" in basename(info.get("_conda_exe", "")) + ): + if url.lower().endswith(".tar.bz2"): + url = url[:-8] + ".conda" + return url + + def get_final_url(info, url): mapping = info.get('channels_remap', []) for entry in mapping: diff --git a/examples/miniforge/construct.yaml b/examples/miniforge/construct.yaml new file mode 100644 index 00000000..b9354081 --- /dev/null +++ b/examples/miniforge/construct.yaml @@ -0,0 +1,21 @@ +name: Miniforge3 +version: 4.10.1-0 +company: conda-forge + +channels: + - conda-forge + +write_condarc: True +keep_pkgs: True +transmute_file_type: .conda + +specs: + - python 3.9.* + - conda 4.10.1 + - pip + - miniforge_console_shortcut 1.* # [win] + +# Added for extra testing +installer_type: all +post_install: test_install.sh # [unix] +post_install: test_install.bat # [win] diff --git a/examples/miniforge/test_install.bat b/examples/miniforge/test_install.bat new file mode 100644 index 00000000..dff126d6 --- /dev/null +++ b/examples/miniforge/test_install.bat @@ -0,0 +1,7 @@ +@ECHO ON +call "%PREFIX%\Scripts\activate.bat +conda install -yq jq || exit 1 +conda config --show-sources || exit 1 +conda config --json --show | jq -r ".channels[0]" > temp.txt +set /p OUTPUT= + +### Bug fixes + +* Add tests for `--conda-exe=` and fix found issues on Linux and macOS. + Not supported on Windows yet. (#503, #605) + +### Deprecations + +* + +### Docs + +* + +### Other + +* diff --git a/scripts/run_examples.py b/scripts/run_examples.py old mode 100755 new mode 100644 index b929f3d6..7a00cc42 --- a/scripts/run_examples.py +++ b/scripts/run_examples.py @@ -1,8 +1,6 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- """Run examples bundled with this repo.""" - -# Standard library imports +import argparse import os import subprocess import sys @@ -11,7 +9,6 @@ import shutil import time from datetime import timedelta - from pathlib import Path from constructor.utils import rm_rf @@ -32,10 +29,15 @@ WITH_SPACES = {"extra_files", "noconda", "signing", "scripts"} -def _execute(cmd): +def _execute(cmd, **env_vars): print(' '.join(cmd)) t0 = time.time() - p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True) + if env_vars: + env = os.environ.copy() + env.update(env_vars) + else: + env = None + p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, env=env) try: stdout, stderr = p.communicate(timeout=420) errored = p.returncode != 0 @@ -45,7 +47,7 @@ def _execute(cmd): print('--- TEST TIMEOUT ---') errored = True t1 = time.time() - if errored: + if errored or "CONDA_VERBOSITY" in env_vars: print(f'--- RETURNCODE: {p.returncode} ---') if stdout: print('--- STDOUT ---') @@ -57,7 +59,7 @@ def _execute(cmd): return errored -def run_examples(keep_artifacts=None): +def run_examples(keep_artifacts=None, conda_exe=None, debug=False): """Run examples bundled with the repository. Parameters @@ -93,21 +95,36 @@ def run_examples(keep_artifacts=None): if os.path.exists(os.path.join(fpath, 'construct.yaml')): example_paths.append(fpath) - # NSIS won't error out when running scripts unless we set this custom environment variable + # NSIS won't error out when running scripts unless + # we set this custom environment variable os.environ["NSIS_SCRIPTS_RAISE_ERRORS"] = "1" parent_output = tempfile.mkdtemp() tested_files = set() which_errored = {} for example_path in sorted(example_paths): - print(example_path) - print('-' * len(example_path)) - output_dir = tempfile.mkdtemp(dir=parent_output) + example_name = Path(example_path).name + test_with_spaces = example_name in WITH_SPACES + print(example_name) + print('-' * len(example_name)) + if ( + sys.platform.startswith("win") + and conda_exe + and "micromamba" in os.path.basename(conda_exe).lower() + ): + print( + f"! Skipping {example_name}... Shortcut creation on Windows is " + "not supported with micromamba." + ) + continue + output_dir = tempfile.mkdtemp(prefix=f"{example_name}-", dir=parent_output) # resolve path to avoid some issues with TEMPDIR on Windows output_dir = str(Path(output_dir).resolve()) - example_name = Path(example_path).parent.name - test_with_spaces = example_name in WITH_SPACES cmd = COV_CMD + ['constructor', '-v', example_path, '--output-dir', output_dir] + if conda_exe: + cmd += ['--conda-exe', conda_exe] + if debug: + cmd.append("--debug") creation_errored = _execute(cmd) errored += creation_errored for fpath in os.listdir(output_dir): @@ -118,8 +135,8 @@ def run_examples(keep_artifacts=None): test_suffix = "s p a c e s" if test_with_spaces else None env_dir = tempfile.mkdtemp(suffix=test_suffix, dir=output_dir) rm_rf(env_dir) - print('--- Testing %s' % fpath) fpath = os.path.join(output_dir, fpath) + print('--- Testing', os.path.basename(fpath)) if ext == 'sh': cmd = ['/bin/sh', fpath, '-b', '-p', env_dir] elif ext == 'pkg': @@ -144,7 +161,8 @@ def run_examples(keep_artifacts=None): # would be enough too :) # This is why we have this weird .split() thingy down here: cmd = ['cmd.exe', '/c', 'start', '/wait', fpath, '/S', *f'/D={env_dir}'.split()] - test_errored = _execute(cmd) + env = {"CONDA_VERBOSITY": "3"} if debug else {} + test_errored = _execute(cmd, **env) # Windows EXEs never throw a non-0 exit code, so we need to check the logs, # which are only written if a special NSIS build is used win_error_lines = [] @@ -220,6 +238,9 @@ def run_examples(keep_artifacts=None): which_errored.setdefault(example_path, []).append("Could not find uninstaller!") if keep_artifacts: + dest = os.path.join(keep_artifacts, os.path.basename(fpath)) + if os.path.isfile(dest): + os.unlink(dest) shutil.move(fpath, keep_artifacts) if creation_errored: which_errored.setdefault(example_path, []).append("Could not create installer!") @@ -231,18 +252,29 @@ def run_examples(keep_artifacts=None): for installer, reasons in which_errored.items(): print(f"+ {os.path.basename(installer)}") for reason in reasons: - print(f"---> {os.path.basename(reason)}") - print('Assets saved in: %s' % parent_output) + print(f"---> {reason}") + print('Assets saved in:', keep_artifacts or parent_output) else: print('All examples ran successfully!') shutil.rmtree(parent_output) return errored +def cli(): + p = argparse.ArgumentParser() + p.add_argument("--keep-artifacts") + p.add_argument("--conda-exe") + p.add_argument("--debug", action="store_true", default=False) + return p.parse_args() + + if __name__ == '__main__': - if len(sys.argv) >= 2 and sys.argv[1].startswith('--keep-artifacts='): - keep_artifacts = sys.argv[1].split("=")[1] - else: - keep_artifacts = None - n_errors = run_examples(keep_artifacts) + args = cli() + if args.conda_exe: + assert os.path.isfile(args.conda_exe) + n_errors = run_examples( + keep_artifacts=args.keep_artifacts, + conda_exe=args.conda_exe, + debug=args.debug + ) sys.exit(n_errors)