Skip to content

Commit f43ee5d

Browse files
committed
Fixes to mache.deploy.bootstrap and mache.jigsaw
The bootstrap fixes: * Bootstrap now isolates nested Pixi calls from any outer Pixi session by unsetting PIXI_PROJECT_MANIFEST, PIXI_PROJECT_ROOT, PIXI_ENVIRONMENT_NAME, and PIXI_IN_SHELL before running Pixi commands. That prevents the workflow/bootstrap and JIGSAW paths from accidentally inheriting the wrong manifest or environment. * The local --mache-fork/--mache-branch bootstrap path now writes a slim deploy_tmp/bootstrap_pixi/pixi.toml instead of copying the whole repo manifest. It keeps the source repo’s workspace channels and top-level dependencies, pins only the requested Python for the current platform, and avoids pulling in CI environment definitions. * Local-source bootstrap no longer uses the live worktree directly. It now snapshots the local mache source into deploy_tmp/build_mache/mache, preferring tracked files from git ls-files, so unrelated untracked files or symlinks in the worktree do not break pip install .. * The bootstrap pixi install steps now use a small retry wrapper, aimed at transient network failures like the connection reset / DNS issues you were seeing in workflow runs. * A few constants/helpers were added or cleaned up to support that behavior, including the module-level bootstrap tool specs (setuptools, wheel) and the helper that builds the Pixi-env-unsetting shell prefix.
1 parent 017d1f8 commit f43ee5d

File tree

2 files changed

+178
-45
lines changed

2 files changed

+178
-45
lines changed

mache/deploy/bootstrap.py

Lines changed: 155 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import shutil
1010
import subprocess
1111
import sys
12+
import time
1213
from pathlib import Path
1314
from typing import Dict # noqa: F401
1415

@@ -27,6 +28,8 @@
2728
'PIXI_ENVIRONMENT_NAME',
2829
'PIXI_IN_SHELL',
2930
)
31+
BOOTSTRAP_SETUPTOOLS_SPEC = '>=60'
32+
BOOTSTRAP_WHEEL_SPEC = '*'
3033

3134

3235
def check_call(
@@ -210,6 +213,39 @@ def check_call(
210213
return result
211214

212215

216+
def check_call_with_retries(
217+
commands,
218+
log_filename,
219+
quiet,
220+
*,
221+
retries=3,
222+
retry_delay=2.0,
223+
):
224+
"""Run a command with a few retries for transient pixi/network failures."""
225+
226+
last_error = None
227+
for attempt in range(1, retries + 1):
228+
try:
229+
return check_call(commands, log_filename, quiet)
230+
except subprocess.CalledProcessError as exc:
231+
last_error = exc
232+
if attempt >= retries:
233+
raise
234+
235+
message = (
236+
f'Command failed on attempt {attempt}/{retries}; '
237+
f'retrying in {retry_delay:.0f}s...\n'
238+
)
239+
with open(log_filename, 'a', encoding='utf-8') as log_file:
240+
log_file.write(message)
241+
if not quiet:
242+
print(message)
243+
time.sleep(retry_delay)
244+
245+
if last_error is not None:
246+
raise last_error
247+
248+
213249
def build_pixi_shell_hook_prefix(*, pixi_exe: str, pixi_toml: str) -> str:
214250
"""Build a shell prefix to activate a pixi env in the current shell.
215251
@@ -369,14 +405,15 @@ def _run(log_filename):
369405
_copy_mache_pixi_toml(
370406
dest_pixi_toml=pixi_toml_path,
371407
source_repo_dir=Path('deploy_tmp/build_mache/mache'),
408+
python_version=args.python,
372409
)
373410

374411
cmd_install = (
375412
f'cd "{bootstrap_dir}" && '
376413
f'{build_pixi_env_unset_prefix()} '
377414
f'"{pixi_exe}" install'
378415
)
379-
check_call(cmd_install, log_filename, quiet)
416+
check_call_with_retries(cmd_install, log_filename, quiet)
380417

381418
pixi_toml = str(pixi_toml_path.resolve())
382419
pixi_shell_hook_prefix = build_pixi_shell_hook_prefix(
@@ -403,7 +440,7 @@ def _run(log_filename):
403440
f'{build_pixi_env_unset_prefix()} '
404441
f'"{pixi_exe}" install'
405442
)
406-
check_call(cmd_install, log_filename, quiet)
443+
check_call_with_retries(cmd_install, log_filename, quiet)
407444

408445

409446
def _parse_args():
@@ -735,13 +772,63 @@ def _format_pixi_version_specifier(version: str) -> str:
735772
return f'=={normalized}'
736773

737774

738-
def _copy_mache_pixi_toml(*, dest_pixi_toml, source_repo_dir):
775+
def _copy_mache_pixi_toml(*, dest_pixi_toml, source_repo_dir, python_version):
739776
src = Path(source_repo_dir) / 'pixi.toml'
740777
if not src.is_file():
741778
raise RuntimeError(
742779
f'Expected mache pixi.toml not found in cloned repo: {src}'
743780
)
744-
shutil.copyfile(str(src), str(dest_pixi_toml))
781+
source_text = src.read_text(encoding='utf-8')
782+
channels = _get_pixi_channels_from_text(source_text)
783+
dependencies = _get_pixi_dependencies_from_text(source_text)
784+
785+
_write_bootstrap_pixi_toml_with_local_source(
786+
pixi_toml_path=Path(dest_pixi_toml),
787+
channels=channels,
788+
dependencies=dependencies,
789+
python_version=python_version,
790+
)
791+
792+
793+
def _write_bootstrap_pixi_toml_with_local_source(
794+
*,
795+
pixi_toml_path: Path,
796+
channels: list[str],
797+
dependencies: dict[str, str],
798+
python_version: str,
799+
) -> None:
800+
"""Write a slim bootstrap pixi manifest for a local mache source tree.
801+
802+
The full repo ``pixi.toml`` may contain CI-only features, named
803+
environments, and multiple platforms. The bootstrap environment only needs
804+
the current platform, the requested Python, and the runtime/build
805+
dependencies required to install the local mache checkout.
806+
"""
807+
808+
merged_channels = channels[:] if channels else ['conda-forge']
809+
merged_dependencies = dict(dependencies)
810+
merged_dependencies.setdefault('pip', '*')
811+
merged_dependencies.setdefault('rattler-build', '*')
812+
merged_dependencies.setdefault('setuptools', BOOTSTRAP_SETUPTOOLS_SPEC)
813+
merged_dependencies.setdefault('wheel', BOOTSTRAP_WHEEL_SPEC)
814+
815+
lines = [
816+
'[workspace]',
817+
'name = "mache-bootstrap-local"',
818+
f'channels = [{", ".join(json.dumps(c) for c in merged_channels)}]',
819+
f'platforms = ["{_get_pixi_platform()}"]',
820+
'channel-priority = "strict"',
821+
'',
822+
'[dependencies]',
823+
f'python = "{python_version}.*"',
824+
]
825+
826+
for name, spec in merged_dependencies.items():
827+
if name == 'python':
828+
continue
829+
lines.append(f'{name} = {json.dumps(spec)}')
830+
831+
pixi_toml_path.write_text('\n'.join(lines) + '\n', encoding='utf-8')
745832

746833

747834
def merge_pixi_toml_dependencies(
@@ -767,10 +854,7 @@ def merge_pixi_toml_dependencies(
767854

768855
source_text = source_pixi_toml.read_text(encoding='utf-8')
769856
channels = _get_pixi_channels_from_text(source_text)
770-
dependencies = _get_pixi_dependencies_from_text(
771-
source_text,
772-
python_version=python_version,
773-
)
857+
dependencies = _get_pixi_dependencies_from_text(source_text)
774858

775859
target_path = Path(target_pixi_toml)
776860
text = target_path.read_text(encoding='utf-8')
@@ -793,28 +877,24 @@ def _get_pixi_channels_from_text(source_text):
793877
return re.findall(r'"([^"]+)"', match.group(1))
794878

795879

796-
def _get_pixi_dependencies_from_text(source_text, *, python_version):
880+
def _get_pixi_dependencies_from_text(source_text):
797881
dependencies = {} # type: Dict[str, str]
798882

799-
for table in (
800-
'dependencies',
801-
f'feature.py{python_version.replace(".", "")}.dependencies',
883+
section = _find_toml_table_block(text=source_text, table='dependencies')
884+
if section is None:
885+
return dependencies
886+
887+
start, end = section
888+
section_text = source_text[start:end]
889+
for match in re.finditer(
890+
r'(?m)^([A-Za-z0-9_.-]+)\s*=\s*"([^"\n]+)"\s*$',
891+
section_text,
802892
):
803-
section = _find_toml_table_block(text=source_text, table=table)
804-
if section is None:
893+
name = match.group(1)
894+
spec = match.group(2)
895+
if name == 'python':
805896
continue
806-
807-
start, end = section
808-
section_text = source_text[start:end]
809-
for match in re.finditer(
810-
r'(?m)^([A-Za-z0-9_.-]+)\s*=\s*"([^"\n]+)"\s*$',
811-
section_text,
812-
):
813-
name = match.group(1)
814-
spec = match.group(2)
815-
if name == 'python':
816-
continue
817-
dependencies.setdefault(name, spec)
897+
dependencies.setdefault(name, spec)
818898

819899
return dependencies
820900

@@ -925,20 +1005,10 @@ def _clone_mache_repo(
9251005
f'Local mache source override does not exist: {source_repo}'
9261006
)
9271007

928-
try:
929-
os.symlink(source_repo, repo_dir, target_is_directory=True)
930-
except OSError:
931-
shutil.copytree(
932-
source_repo,
933-
repo_dir,
934-
ignore=shutil.ignore_patterns(
935-
'.git',
936-
'.pixi',
937-
'deploy_tmp',
938-
'__pycache__',
939-
'*.pyc',
940-
),
941-
)
1008+
_copy_local_mache_source_snapshot(
1009+
source_repo=source_repo,
1010+
repo_dir=repo_dir,
1011+
)
9421012
return
9431013

9441014
commands = (
@@ -950,5 +1020,50 @@ def _clone_mache_repo(
9501020
check_call(commands, log_filename, quiet)
9511021

9521022

1023+
def _copy_local_mache_source_snapshot(
1024+
*, source_repo: Path, repo_dir: Path
1025+
) -> None:
1026+
"""Copy a clean local source snapshot for bootstrap installs.
1027+
1028+
Using the live developer worktree directly can pull in untracked files or
1029+
local symlinks that break setuptools packaging. Prefer a tracked-file
1030+
snapshot when the source is a git checkout, and fall back to a filtered
1031+
copytree otherwise.
1032+
"""
1033+
1034+
try:
1035+
tracked = subprocess.check_output(
1036+
['git', '-C', str(source_repo), 'ls-files', '-z'],
1037+
text=False,
1038+
)
1039+
except (OSError, subprocess.CalledProcessError):
1040+
shutil.copytree(
1041+
source_repo,
1042+
repo_dir,
1043+
ignore=shutil.ignore_patterns(
1044+
'.git',
1045+
'.pixi',
1046+
'deploy_tmp',
1047+
'__pycache__',
1048+
'*.pyc',
1049+
),
1050+
)
1051+
return
1052+
1053+
repo_dir.mkdir(parents=True, exist_ok=True)
1054+
for raw_path in tracked.split(b'\x00'):
1055+
if not raw_path:
1056+
continue
1057+
rel_path = Path(raw_path.decode('utf-8'))
1058+
src_path = source_repo / rel_path
1059+
dest_path = repo_dir / rel_path
1060+
dest_path.parent.mkdir(parents=True, exist_ok=True)
1061+
1062+
if src_path.is_symlink():
1063+
os.symlink(os.readlink(src_path), dest_path)
1064+
else:
1065+
shutil.copy2(src_path, dest_path)
1066+
1067+
9531068
if __name__ == '__main__':
9541069
main()

mache/jigsaw/__init__.py

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,11 @@
1313

1414
from jinja2 import Environment, StrictUndefined
1515

16-
from mache.deploy.bootstrap import check_call
16+
from mache.deploy.bootstrap import (
17+
build_pixi_env_unset_prefix,
18+
check_call,
19+
check_call_with_retries,
20+
)
1721

1822
JIGSAW_PYTHON_URL = 'git@github.com:dengwirda/jigsaw-python.git'
1923

@@ -715,7 +719,7 @@ def _build_external_jigsaw(
715719
platform_name=platform_name,
716720
)
717721
command = (
718-
'env -u PIXI_PROJECT_MANIFEST -u PIXI_PROJECT_ROOT '
722+
f'{build_pixi_env_unset_prefix()} '
719723
f'{shlex.quote(pixi_exe)} run -m {shlex.quote(str(pixi_toml))} '
720724
'rattler-build build '
721725
f'--recipe-dir {shlex.quote(str(recipe_dir.resolve()))} '
@@ -758,7 +762,11 @@ def _ensure_conda_rattler_build_env(
758762
'--channel conda-forge '
759763
'rattler-build'
760764
)
761-
check_call(command, log_filename=log_filename, quiet=quiet)
765+
check_call_with_retries(
766+
command,
767+
log_filename=log_filename,
768+
quiet=quiet,
769+
)
762770

763771
return f'{shlex.quote(conda)} run --prefix {shlex.quote(str(tool_prefix))}'
764772

@@ -931,22 +939,32 @@ def _install_into_pixi(
931939
package_spec = _format_pixi_jigsaw_spec(jigsaw_version)
932940

933941
add_channel_command = (
942+
f'{build_pixi_env_unset_prefix()} '
934943
f'{shlex.quote(pixi_exe)} workspace channel add '
935944
f'--manifest-path {shlex.quote(manifest)} '
936945
f'{feature_arg}'
937946
'--prepend '
938947
f'{shlex.quote(channel_uri)}'
939948
)
940-
check_call(add_channel_command, log_filename=log_filename, quiet=quiet)
949+
check_call_with_retries(
950+
add_channel_command,
951+
log_filename=log_filename,
952+
quiet=quiet,
953+
)
941954

942955
add_package_command = (
956+
f'{build_pixi_env_unset_prefix()} '
943957
f'{shlex.quote(pixi_exe)} add '
944958
f'--manifest-path {shlex.quote(manifest)} '
945959
f'{platform_arg}'
946960
f'{feature_arg}'
947961
f'{shlex.quote(package_spec)}'
948962
)
949-
check_call(add_package_command, log_filename=log_filename, quiet=quiet)
963+
check_call_with_retries(
964+
add_package_command,
965+
log_filename=log_filename,
966+
quiet=quiet,
967+
)
950968

951969

952970
def _infer_pixi_feature_for_active_environment(

0 commit comments

Comments
 (0)