Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

PR: Use conda-lock files to incrementally update conda-based installers #22059

Merged
merged 18 commits into from
May 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/scripts/installer_test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ check_prefix() {
echo "\nContents of ${base_prefix}:"
ls -al $base_prefix
else
echo "$base_prefix does not exist!"
echo "Base prefix does not exist!"
exit 1
fi
}
Expand Down Expand Up @@ -66,7 +66,7 @@ check_shortcut() {
fi
}

install
install || exit 1
echo "Install info:"
check_prefix
check_uninstall
Expand Down
31 changes: 19 additions & 12 deletions .github/workflows/installers-conda.yml
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,10 @@ jobs:
if: runner.os == 'Windows'
uses: mamba-org/setup-micromamba@v1
with:
condarc: |
conda_build:
pkg_format: '2'
zstd_compression_level: '19'
environment-file: installers-conda/build-environment.yml
environment-name: spy-inst
create-args: >-
Expand All @@ -184,6 +188,10 @@ jobs:
if: runner.os != 'Windows'
uses: mamba-org/setup-micromamba@v1
with:
condarc: |
conda_build:
pkg_format: '2'
zstd_compression_level: '19'
environment-file: installers-conda/build-environment.yml
environment-name: spy-inst
create-args: >-
Expand Down Expand Up @@ -245,14 +253,13 @@ jobs:
run: |
[[ -n $CNAME ]] && args=("--cert-id" "$CNAME") || args=()
python build_installers.py ${args[@]}
PKG_PATH=$(python build_installers.py --artifact-name)
PKG_NAME=$(basename $PKG_PATH)
PKG_GLOB=${PKG_PATH%.*}
PKG_BASE_NAME=${PKG_NAME%.*}
echo "PKG_PATH=$PKG_PATH" >> $GITHUB_ENV

PKG_NAME=$(ls $DISTDIR | grep Spyder-)
LCK_NAME=$(ls $DISTDIR | grep conda-)
echo "PKG_NAME=$PKG_NAME" >> $GITHUB_ENV
echo "PKG_GLOB=$PKG_GLOB" >> $GITHUB_ENV
echo "PKG_BASE_NAME=$PKG_BASE_NAME" >> $GITHUB_ENV
echo "LCK_NAME=$LCK_NAME" >> $GITHUB_ENV
echo "ARTIFACT_NAME=${PKG_NAME%.*}" >> $GITHUB_ENV
echo "PKG_PATH=$DISTDIR/$PKG_NAME" >> $GITHUB_ENV

- name: Test macOS or Linux Installer
if: runner.os != 'Windows'
Expand Down Expand Up @@ -286,16 +293,16 @@ jobs:
if [[ $RUNNER_OS == "macOS" ]]; then
./notarize.sh -p $APPLICATION_PWD $PKG_PATH
else
cd $(dirname $PKG_PATH)
echo $(sha256sum $PKG_NAME) > "${PKG_GLOB}-sha256sum.txt"
cd $DISTDIR
echo $(sha256sum $PKG_NAME) > "${ARTIFACT_NAME}-sha256sum.txt"
fi

- name: Upload Artifact
if: env.IS_RELEASE == 'false'
uses: actions/upload-artifact@v4
with:
path: ${{ env.PKG_GLOB }}*
name: ${{ env.PKG_BASE_NAME }}
path: ${{ env.DISTDIR }}
name: ${{ env.ARTIFACT_NAME }}

- name: Get Release
if: env.IS_RELEASE == 'true'
Expand All @@ -311,4 +318,4 @@ jobs:
GITHUB_TOKEN: ${{ github.token }}
with:
upload_url: ${{ steps.get_release.outputs.upload_url }}
asset_path: ${{ env.PKG_GLOB }}*
asset_path: ${{ env.DISTDIR }}/*
1 change: 1 addition & 0 deletions installers-conda/build-environment.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ channels:
dependencies:
- boa <0.17.0 # See https://github.com/conda-forge/boa-feedstock/issues/81
- conda >=23.11.0
- conda-lock >=2.5.7
- conda-standalone >=23.11.0
- constructor >=3.6.0
- gitpython
Expand Down
3 changes: 2 additions & 1 deletion installers-conda/build_conda_pkgs.py
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,8 @@ def build(self):
f"{self.name}={self.version}...")
check_call([
"conda", "mambabuild",
"--no-test", "--skip-existing", "--build-id-pat={n}",
"--skip-existing", "--build-id-pat={n}",
"--no-test", "--no-anaconda-upload",
str(self._fdstk_path / "recipe")
])
finally:
Expand Down
153 changes: 116 additions & 37 deletions installers-conda/build_installers.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,10 @@

# Third-party imports
from ruamel.yaml import YAML
from setuptools_scm import get_version

# Local imports
from build_conda_pkgs import HERE, BUILD, RESOURCES, SPECS, h, get_version
from build_conda_pkgs import HERE, BUILD, RESOURCES, SPECS, h

DIST = HERE / "dist"

Expand All @@ -56,34 +57,28 @@
WINDOWS = os.name == "nt"
MACOS = sys.platform == "darwin"
LINUX = sys.platform.startswith("linux")
TARGET_PLATFORM = os.environ.get("CONSTRUCTOR_TARGET_PLATFORM")
CONDA_BLD_PATH = os.getenv("CONDA_BLD_PATH", "local")
PY_VER = "{v.major}.{v.minor}.{v.micro}".format(v=sys.version_info)
SPYVER = get_version(SPYREPO).split("+")[0]

if TARGET_PLATFORM == "osx-arm64":
ARCH = "arm64"
else:
ARCH = (platform.machine() or "generic").lower().replace("amd64", "x86_64")
if WINDOWS:
OS = "Windows"
TARGET_PLATFORM = "win-"
INSTALL_CHOICES = ["exe"]
elif LINUX:
OS = "Linux"
TARGET_PLATFORM = "linux-"
INSTALL_CHOICES = ["sh"]
elif MACOS:
OS = "macOS"
TARGET_PLATFORM = "osx-"
INSTALL_CHOICES = ["pkg", "sh"]
else:
raise RuntimeError(f"Unrecognized OS: {sys.platform}")

scientific_packages = {
"cython": "",
"matplotlib": "",
"numpy": "",
"openpyxl": "",
"pandas": "",
"scipy": "",
"sympy": "",
}
ARCH = (platform.machine() or "generic").lower().replace("amd64", "x86_64")
TARGET_PLATFORM = (TARGET_PLATFORM + ARCH).replace("x86_64", "64")
TARGET_PLATFORM = os.getenv("CONSTRUCTOR_TARGET_PLATFORM", TARGET_PLATFORM)

# ---- Parse arguments
p = ArgumentParser()
Expand All @@ -109,7 +104,7 @@
)
p.add_argument(
"--extra-specs", nargs="+", default=[],
help="One or more extra conda specs to add to the installer",
help="One or more extra conda specs to add to the installer.",
)
p.add_argument(
"--licenses", action="store_true",
Expand All @@ -128,19 +123,27 @@
"--install-type", choices=INSTALL_CHOICES, default=INSTALL_CHOICES[0],
help="Installer type."
)
p.add_argument(
"--conda-lock", action="store_true",
help="Create conda-lock file and exit."
)
args = p.parse_args()

yaml = YAML()
yaml.indent(mapping=2, sequence=4, offset=2)
indent4 = partial(indent, prefix=" ")

SPYVER = get_version(SPYREPO, normalize=False).lstrip('v').split("+")[0]

specs = {
"python": "=" + PY_VER,
"spyder": "=" + SPYVER,
"cython": "",
"matplotlib": "",
"numpy": "",
"openpyxl": "",
"pandas": "",
"scipy": "",
"sympy": "",
}
specs.update(scientific_packages)

if SPECS.exists():
logger.info(f"Reading specs from {SPECS}...")
Expand All @@ -150,17 +153,73 @@
logger.info(f"Did not read specs from {SPECS}")

for spec in args.extra_specs:
k, *v = re.split('([<>= ]+)', spec)
k, *v = re.split('([<>=]+)[ ]*', spec)
specs[k] = "".join(v).strip()
if k == "spyder":
SPYVER = v[-1]

PY_VER = re.split('([<>=]+)[ ]*', specs['python'])[-1]
SPYVER = re.split('([<>=]+)[ ]*', specs['spyder'])[-1]

LOCK_FILE = DIST / f"conda-{TARGET_PLATFORM}.lock"
TMP_LOCK_FILE = BUILD / f"conda-{TARGET_PLATFORM}.lock"
OUTPUT_FILE = DIST / f"{APP}-{OS}-{ARCH}.{args.install_type}"
INSTALLER_DEFAULT_PATH_STEM = f"{APP.lower()}-{SPYVER.split('.')[0]}"

WELCOME_IMG_WIN = BUILD / "welcome_img_win.png"
HEADER_IMG_WIN = BUILD / "header_img_win.png"
WELCOME_IMG_MAC = BUILD / "welcome_img_mac.png"
CONSTRUCTOR_FILE = BUILD / "construct.yaml"


def _create_conda_lock():
definitions = {
"channels": [
CONDA_BLD_PATH,
"conda-forge/label/spyder_dev",
"conda-forge/label/spyder_kernels_rc",
"conda-forge"
],
"dependencies": [k + v for k, v in specs.items()],
"platforms": [TARGET_PLATFORM]
}

logger.info("Conda lock configuration:")
if logger.getEffectiveLevel() <= 20:
yaml.dump(definitions, sys.stdout)

env_file = BUILD / "runtime_env.yml"
yaml.dump(definitions, env_file)

env = os.environ.copy()
env["CONDA_CHANNEL_PRIORITY"] = "flexible"

cmd_args = [
"conda-lock", "lock",
"--kind", "explicit",
"--file", str(env_file),
"--filename-template", str(BUILD / "conda-{platform}.lock")
# Note conda-lock doesn't provide output file option, only template
]

run(cmd_args, check=True, env=env)

logger.info(f"Contents of {TMP_LOCK_FILE}:")
if logger.getEffectiveLevel() <= 20:
print(TMP_LOCK_FILE.read_text(), flush=True)


def _patch_conda_lock():
# Replace local channel url with conda-forge and remove checksum
tmp_text = TMP_LOCK_FILE.read_text()
text = re.sub(
f"^{_get_conda_bld_path_url()}(.*)#.*$",
r"https://conda.anaconda.org/conda-forge\1",
tmp_text, flags=re.MULTILINE
)
LOCK_FILE.write_text(text)

logger.info(f"Contents of {LOCK_FILE}:")
if logger.getEffectiveLevel() <= 20:
print(LOCK_FILE.read_text(), flush=True)


def _generate_background_images(installer_type):
Expand Down Expand Up @@ -198,15 +257,16 @@ def _get_condarc():
contents = dedent(
"""
channels: #!final
- conda-forge/label/spyder_kernels_rc
- conda-forge/label/spyder_dev
- conda-forge/label/spyder_kernels_rc
- conda-forge
repodata_fns: #!final
- repodata.json
auto_update_conda: false #!final
notify_outdated_conda: false #!final
channel_priority: flexible #!final
env_prompt: '[spyder]({default_env}) ' #! final
register_envs: false #! final
"""
)
# the undocumented #!final comment is explained here
Expand All @@ -233,6 +293,7 @@ def _definitions():
"reverse_domain_identifier": "org.spyder-ide.Spyder",
"version": SPYVER,
"channels": [
"conda-forge/label/spyder_dev",
"conda-forge/label/spyder_kernels_rc",
"conda-forge",
],
Expand All @@ -251,8 +312,8 @@ def _definitions():
"register_envs": False,
"extra_envs": {
"spyder-runtime": {
"specs": [k + v for k, v in specs.items()],
},
"environment_file": str(TMP_LOCK_FILE),
}
},
"channels_remap": [
{
Expand Down Expand Up @@ -354,7 +415,11 @@ def _definitions():
if definitions.get("welcome_image") or definitions.get("header_image"):
_generate_background_images(definitions.get("installer_type", "all"))

return definitions
logger.info(f"Contents of {CONSTRUCTOR_FILE}:")
if logger.getEffectiveLevel() <= 20:
yaml.dump(definitions, sys.stdout)

yaml.dump(definitions, CONSTRUCTOR_FILE)


def _constructor():
Expand All @@ -366,24 +431,24 @@ def _constructor():
if not constructor:
raise RuntimeError("Constructor must be installed and in PATH.")

definitions = _definitions()
_definitions()

cmd_args = [constructor, "-v", "--output-dir", str(DIST)]
cmd_args = [
constructor, "-v",
"--output-dir", str(DIST),
"--platform", TARGET_PLATFORM,
str(CONSTRUCTOR_FILE.parent)
]
if args.debug:
cmd_args.append("--debug")
conda_exe = os.environ.get("CONSTRUCTOR_CONDA_EXE")
if TARGET_PLATFORM and conda_exe:
cmd_args += ["--platform", TARGET_PLATFORM, "--conda-exe", conda_exe]
cmd_args.append(str(BUILD))
conda_exe = os.getenv("CONSTRUCTOR_CONDA_EXE")
if conda_exe:
cmd_args.extend(["--conda-exe", conda_exe])

env = os.environ.copy()
env["CONDA_CHANNEL_PRIORITY"] = "flexible"

logger.info("Command: " + " ".join(cmd_args))
logger.info("Configuration:")
yaml.dump(definitions, sys.stdout)

yaml.dump(definitions, BUILD / "construct.yaml")

run(cmd_args, check=True, env=env)

Expand Down Expand Up @@ -418,13 +483,23 @@ def main():
t0 = time()
try:
DIST.mkdir(exist_ok=True)
_create_conda_lock()
assert TMP_LOCK_FILE.exists()
finally:
elapse = timedelta(seconds=int(time() - t0))
logger.info(f"Build time: {elapse}")

t0 = time()
try:
_constructor()
assert Path(OUTPUT_FILE).exists()
assert OUTPUT_FILE.exists()
logger.info(f"Created {OUTPUT_FILE}")
finally:
elapse = timedelta(seconds=int(time() - t0))
logger.info(f"Build time: {elapse}")

_patch_conda_lock()


if __name__ == "__main__":
if args.arch:
Expand All @@ -442,5 +517,9 @@ def main():
if args.images:
_generate_background_images()
sys.exit()
if args.conda_lock:
_create_conda_lock()
_patch_conda_lock()
sys.exit()

main()
Loading