commit d01cf91e8296a9bb2dcfab1bd7f1871c2c4cc9bc Author: Ryan Volz Date: Sat Mar 6 18:49:28 2021 -0500 Initial radioconda env file and render/build scripts. diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2dc658c --- /dev/null +++ b/.gitignore @@ -0,0 +1,140 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +.vscode/ diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..941aa59 --- /dev/null +++ b/LICENSE @@ -0,0 +1,35 @@ +Radioconda installer code uses BSD-3-Clause license as stated below. +Binary packages that come with radioconda have their own licensing terms +and by installing radioconda you agree to the licensing terms of individual +packages as well. These additional licenses can be found in the +pkgs//info/licenses folder of your radioconda installation. + +============================================================================= + +Copyright (c) 2021, Ryan Volz +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this +list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, +this list of conditions and the following disclaimer in the documentation +and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its contributors +may be used to endorse or promote products derived from this software without +specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/build_installer.py b/build_installer.py new file mode 100755 index 0000000..39afd80 --- /dev/null +++ b/build_installer.py @@ -0,0 +1,78 @@ +#!/usr/bin/env python3 +import pathlib +import re + +platform_re = re.compile("^.*-(?P[(?:linux)(?:osx)(?:win)].*)$") + + +def spec_dir_extract_platform(installer_spec_dir: pathlib.Path) -> str: + spec_dir_name = installer_spec_dir.name + + platform_match = platform_re.match(spec_dir_name) + + if platform_match: + return platform_match.group("platform") + else: + raise ValueError( + f"Could not identify platform from directory name: {spec_dir_name}" + ) + + +if __name__ == "__main__": + import argparse + import subprocess + import sys + + from constructor import main as constructor_main + + platform = constructor_main.cc_platform + cwd = pathlib.Path(".").absolute() + here = pathlib.Path(__file__).parent.absolute().relative_to(cwd) + + parser = argparse.ArgumentParser( + description=( + "Build installer package(s) using conda constructor." + " Additional command-line options will be passed to constructor." + ) + ) + parser.add_argument( + "installer_spec_dir", + type=pathlib.Path, + nargs="?", + default=here / "installer_specs" / f"radioconda-{platform}", + help=( + "Installer specification directory (containing construct.yaml)" + " for a particular platform (name ends in the platform identifier)." + " (default: %(default)s)" + ), + ) + parser.add_argument( + "-o", + "--output_dir", + type=pathlib.Path, + default=here / "dist", + help=( + "Output directory in which the installer package(s) will be placed." + " (default: %(default)s)" + ), + ) + + args, constructor_args = parser.parse_known_args() + + platform = spec_dir_extract_platform(args.installer_spec_dir) + + constructor_cmdline = [ + "constructor", + args.installer_spec_dir, + "--platform", + platform, + "--output-dir", + args.output_dir, + ] + constructor_args + + proc = subprocess.run(constructor_cmdline) + + try: + proc.check_returncode() + except subprocess.CalledProcessError: + sys.exit(1) diff --git a/radioconda.yaml b/radioconda.yaml new file mode 100644 index 0000000..56acb8f --- /dev/null +++ b/radioconda.yaml @@ -0,0 +1,29 @@ +name: radioconda +channels: + - conda-forge +platforms: + - linux-64 + - osx-64 + - win-64 +dependencies: + - mamba + - python + + - digital_rf + - gnuradio + - gnuradio-osmosdr + - gnuradio-satellites + - gnuradio-soapy + - gqrx + - libiio + #- libm2k + - limesuite + - pyadi-iio + - rtl-sdr + - soapysdr + - soapysdr-module-lms7 + - soapysdr-module-plutosdr + - soapysdr-module-remote + - soapysdr-module-rtlsdr + - soapysdr-module-uhd + - uhd diff --git a/rerender.py b/rerender.py new file mode 100755 index 0000000..df6fac8 --- /dev/null +++ b/rerender.py @@ -0,0 +1,187 @@ +#!/usr/bin/env python3 +import pathlib +import shutil + +import conda_lock +import yaml + + +def render_lock_spec( + lock_spec: conda_lock.src_parser.LockSpecification, conda_exe: str +) -> conda_lock.src_parser.LockSpecification: + create_env_dict = conda_lock.conda_lock.solve_specs_for_arch( + conda=conda_exe, + channels=lock_spec.channels, + specs=lock_spec.specs, + platform=lock_spec.platform, + ) + pkgs = create_env_dict["actions"]["LINK"] + spec_names = set( + spec.split(sep=None, maxsplit=1)[0].split(sep="=", maxsplit=1)[0] + for spec in lock_spec.specs + ) + + rendered_specs = [] + for pkg in pkgs: + if pkg["name"] in spec_names: + rendered_specs.append("{name}={version}={build_string}".format(**pkg)) + + rendered_lock_spec = conda_lock.src_parser.LockSpecification( + specs=sorted(rendered_specs), + channels=lock_spec.channels, + platform=lock_spec.platform, + ) + + return rendered_lock_spec + + +def render_constructor_specs( + environment_file: pathlib.Path, + version: str, + company: str, + license_file: pathlib.Path, + output_dir: pathlib.Path, + conda_exe: str, +) -> dict: + with environment_file.open("r") as f: + env_yaml_data = yaml.safe_load(f) + + installer_name = env_yaml_data["name"] + platforms = env_yaml_data["platforms"] + + if not license_file.exists(): + raise ValueError(f"Cannot find license file: {license_file}") + + output_dir.mkdir(parents=True, exist_ok=True) + + constructor_specs = {} + + for platform in platforms: + constructor_name = f"{installer_name}-{platform}" + + lock_spec = conda_lock.conda_lock.parse_environment_file( + environment_file=environment_file, platform=platform + ) + rendered_lock_spec = render_lock_spec(lock_spec, conda_exe) + construct_dict = dict( + name=installer_name, + version=version, + company=company, + channels=rendered_lock_spec.channels, + specs=rendered_lock_spec.specs, + initialize_by_default=True, + installer_type="all", + keep_pkgs=True, + license_file="LICENSE", + register_python_default=False, + write_condarc=True, + ) + if platform.startswith("win"): + construct_dict["post_install"] = "post_install.bat" + else: + construct_dict["post_install"] = "post_install.sh" + + constructor_specs[constructor_name] = construct_dict + + constructor_dir = output_dir / constructor_name + if constructor_dir.exists(): + shutil.rmtree(constructor_dir) + constructor_dir.mkdir(parents=True) + + # copy license to the constructor directory + shutil.copy(license_file, constructor_dir / "LICENSE") + + # write the post_install scripts referenced in the construct dict + if platform.startswith("win"): + with (constructor_dir / "post_install.bat").open("w") as f: + f.writelines((r"del /q %PREFIX%\pkgs\*.tar.bz2", "")) + else: + with (constructor_dir / "post_install.sh").open("w") as f: + f.writelines((r"rm -f $PREFIX/pkgs/*.tar.bz2", "")) + + construct_yaml_path = constructor_dir / "construct.yaml" + with construct_yaml_path.open("w") as f: + yaml.safe_dump(construct_dict, stream=f) + + return constructor_specs + + +if __name__ == "__main__": + import argparse + import datetime + + here = pathlib.Path(__file__).parent.relative_to("") + + parser = argparse.ArgumentParser( + description=( + "Re-render installer specification directories to be used by conda" + " constructor." + ) + ) + parser.add_argument( + "environment_file", + type=pathlib.Path, + nargs="?", + default=here / "radioconda.yaml", + help=( + "YAML file defining an installer distribution, with a 'name' string and" + " 'channels', 'platforms', and 'dependencies' lists." + " (default: %(default)s)" + ), + ) + parser.add_argument( + "--company", + type=str, + default="github.com/ryanvolz/radioconda", + help=( + "Name of the company/entity who is responsible for the installer." + " (default: %(default)s)" + ), + ) + parser.add_argument( + "-l", + "--license_file", + type=pathlib.Path, + default=here / "LICENSE", + help=( + "File containing the license that applies to the installer." + " (default: %(default)s)" + ), + ) + parser.add_argument( + "-o", + "--output_dir", + type=pathlib.Path, + default=here / "installer_specs", + help=( + "Output directory in which the installer specifications will be rendered." + " (default: %(default)s)" + ), + ) + parser.add_argument( + "--conda-exe", + type=str, + default=None, + help=( + "Path to the conda (or mamba or micromamba) executable to use." + " (default: search for conda/mamba/micromamba)" + ), + ) + + args = parser.parse_args() + + conda_exe = conda_lock.conda_lock.determine_conda_executable( + conda_executable=args.conda_exe, mamba=True, micromamba=True + ) + + dt = datetime.datetime.now() + version = dt.strftime("%Y.%m.%d") + + constructor_specs = render_constructor_specs( + environment_file=args.environment_file, + version=version, + company=args.company, + license_file=args.license_file, + output_dir=args.output_dir, + conda_exe=conda_exe, + )