Skip to content

Commit c05d514

Browse files
committed
Simulate canonical merge #610
2 parents c2d3607 + 8588faa commit c05d514

2 files changed

Lines changed: 96 additions & 16 deletions

File tree

‎Makefile‎

Lines changed: 19 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
PYTHON_VERSION ?= 3.10
22
VENV_DIR ?= .venv
33
BASE_PYTHON ?= $(shell command -v python$(PYTHON_VERSION) 2>/dev/null || command -v python3 2>/dev/null || command -v python 2>/dev/null)
4+
# RUN_PYTHON is quoted at every use below: the checkout path may contain
5+
# spaces, and unquoted it word-splits in sh, which makes ensure-venv
6+
# misreport a perfectly good interpreter as a missing one.
47
RUN_PYTHON = $(shell for dir in "$$VIRTUAL_ENV" "$$CONDA_PREFIX" "$(VENV_DIR)"; do if [ -n "$$dir" ] && [ -x "$$dir/bin/python" ]; then printf "%s/bin/python" "$$dir"; exit 0; elif [ -n "$$dir" ] && [ -x "$$dir/Scripts/python.exe" ]; then printf "%s/Scripts/python.exe" "$$dir"; exit 0; fi; done; if [ "$(OS)" = "Windows_NT" ]; then printf "%s/Scripts/python.exe" "$(VENV_DIR)"; else printf "%s/bin/python" "$(VENV_DIR)"; fi)
5-
PIP = $(RUN_PYTHON) -m pip
8+
PIP = "$(RUN_PYTHON)" -m pip
69

710
.DEFAULT_GOAL := setup
811

@@ -36,13 +39,13 @@ ensure-venv:
3639
exit 1; \
3740
}; \
3841
fi
39-
@$(RUN_PYTHON) -c "import sys; required=tuple(map(int, '$(PYTHON_VERSION)'.split('.')[:2])); raise SystemExit(0 if sys.version_info[:2] >= required else 1)" || { \
42+
@"$(RUN_PYTHON)" -c "import sys; required=tuple(map(int, '$(PYTHON_VERSION)'.split('.')[:2])); raise SystemExit(0 if sys.version_info[:2] >= required else 1)" || { \
4043
echo "ERROR: OpenMontage requires Python $(PYTHON_VERSION)+."; \
41-
echo "Current interpreter is $$($(RUN_PYTHON) -c 'import sys; print(\".\".join(map(str, sys.version_info[:3])))' 2>/dev/null || echo unavailable): $(RUN_PYTHON)"; \
44+
echo "Current interpreter is $$("$(RUN_PYTHON)" -c 'import sys; print(\".\".join(map(str, sys.version_info[:3])))' 2>/dev/null || echo unavailable): $(RUN_PYTHON)"; \
4245
echo "Activate a compatible environment or remove it so make can create $(VENV_DIR)."; \
4346
exit 1; \
4447
}
45-
@$(RUN_PYTHON) -m pip --version >/dev/null 2>&1 || $(RUN_PYTHON) -m ensurepip --upgrade >/dev/null
48+
@"$(RUN_PYTHON)" -m pip --version >/dev/null 2>&1 || "$(RUN_PYTHON)" -m ensurepip --upgrade >/dev/null
4649

4750
venv: ensure-venv
4851
@echo "==> Virtual environment ready."
@@ -70,9 +73,9 @@ setup: bootstrap
7073
@echo " Pulls the 'hyperframes' npm package into the local npx cache so the"
7174
@echo " first render doesn't pay a 30-60s cold-fetch penalty. ~20MB of disk."
7275
@npx --yes hyperframes --version >/dev/null 2>&1 && echo " HyperFrames CLI cached (npx)" || echo " [skip] HyperFrames cache-warm failed — offline or npm unavailable; first render will fetch on demand"
73-
@$(RUN_PYTHON) -c "from tools.video.hyperframes_compose import HyperFramesCompose; HyperFramesCompose._npm_resolve_cache=None; c=HyperFramesCompose()._runtime_check(); print(f' HyperFrames runtime_available={c[\"runtime_available\"]}, npm={c.get(\"npm_package_version\") or c.get(\"npm_resolve_error\")}'); [print(f' note: {r}') for r in c['reasons']]" || echo " [skip] HyperFrames check failed — runtime can be set up later"
76+
@"$(RUN_PYTHON)" -c "from tools.video.hyperframes_compose import HyperFramesCompose; HyperFramesCompose._npm_resolve_cache=None; c=HyperFramesCompose()._runtime_check(); print(f' HyperFrames runtime_available={c[\"runtime_available\"]}, npm={c.get(\"npm_package_version\") or c.get(\"npm_resolve_error\")}'); [print(f' note: {r}') for r in c['reasons']]" || echo " [skip] HyperFrames check failed — runtime can be set up later"
7477
@echo ""
75-
$(RUN_PYTHON) -c "import shutil, os; e=os.path.exists('.env'); shutil.copy('.env.example','.env') if not e else None; print('==> Created .env from .env.example — add your API keys there.' if not e else '==> .env already exists — skipping.')"
78+
"$(RUN_PYTHON)" -c "import shutil, os; e=os.path.exists('.env'); shutil.copy('.env.example','.env') if not e else None; print('==> Created .env from .env.example — add your API keys there.' if not e else '==> .env already exists — skipping.')"
7679
@echo ""
7780
@echo "Done! Open this project in your AI coding assistant and start creating."
7881
@echo " Optional: add API keys to .env to unlock cloud providers."
@@ -95,19 +98,19 @@ install-gpu: bootstrap
9598
# ---- Testing ----
9699

97100
test: ensure-venv
98-
$(RUN_PYTHON) -m pytest tests/ -v
101+
"$(RUN_PYTHON)" -m pytest tests/ -v
99102

100103
test-contracts: ensure-venv
101-
$(RUN_PYTHON) -m pytest tests/contracts/ -v
104+
"$(RUN_PYTHON)" -m pytest tests/contracts/ -v
102105

103106
# ---- Utilities ----
104107

105108
preflight: ensure-venv
106-
$(RUN_PYTHON) -c "from tools.tool_registry import registry; import json; registry.discover(); print(json.dumps(registry.provider_menu(), indent=2))"
109+
"$(RUN_PYTHON)" -c "from tools.tool_registry import registry; import json; registry.discover(); print(json.dumps(registry.provider_menu(), indent=2))"
107110

108111
hyperframes-doctor: ensure-venv
109112
@echo "==> Probing HyperFrames runtime (node/ffmpeg/npx + hyperframes doctor)..."
110-
$(RUN_PYTHON) -c "from tools.video.hyperframes_compose import HyperFramesCompose; r=HyperFramesCompose().execute({'operation':'doctor'}); import json; print(json.dumps(r.data, indent=2)); print('OK' if r.success else f'FAIL: {r.error}')"
113+
"$(RUN_PYTHON)" -c "from tools.video.hyperframes_compose import HyperFramesCompose; r=HyperFramesCompose().execute({'operation':'doctor'}); import json; print(json.dumps(r.data, indent=2)); print('OK' if r.success else f'FAIL: {r.error}')"
111114

112115
hyperframes-warm:
113116
@echo "==> Refreshing the HyperFrames npx cache to latest..."
@@ -119,16 +122,16 @@ demo: ensure-venv
119122
@echo "==> Rendering zero-key demo videos (no API keys needed)..."
120123
@echo " These use only Remotion components — animated charts, text, data viz."
121124
@echo ""
122-
$(RUN_PYTHON) render_demo.py
125+
"$(RUN_PYTHON)" render_demo.py
123126

124127
demo-list: ensure-venv
125-
$(RUN_PYTHON) render_demo.py --list
128+
"$(RUN_PYTHON)" render_demo.py --list
126129

127130
lint: ensure-venv
128-
$(RUN_PYTHON) -m py_compile tools/base_tool.py
129-
$(RUN_PYTHON) -m py_compile tools/tool_registry.py
130-
$(RUN_PYTHON) -m py_compile tools/cost_tracker.py
131-
$(RUN_PYTHON) -m py_compile tools/analysis/composition_validator.py
131+
"$(RUN_PYTHON)" -m py_compile tools/base_tool.py
132+
"$(RUN_PYTHON)" -m py_compile tools/tool_registry.py
133+
"$(RUN_PYTHON)" -m py_compile tools/cost_tracker.py
134+
"$(RUN_PYTHON)" -m py_compile tools/analysis/composition_validator.py
132135

133136
clean:
134137
$(BASE_PYTHON) -c "import pathlib, shutil; excluded=[pathlib.Path('$(VENV_DIR)'), pathlib.Path('venv')]; skip=lambda p: any(p == root or root in p.parents for root in excluded); roots=[p for p in pathlib.Path('.').rglob('__pycache__') if not skip(p)]; [shutil.rmtree(p) for p in roots]; files=[p for p in pathlib.Path('.').rglob('*.pyc') if not skip(p)]; [p.unlink() for p in files]"
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
"""Regression tests for Makefile RUN_PYTHON quoting.
2+
3+
A checkout path may contain spaces (a Windows "Program Files"-style directory,
4+
an external volume, or any user folder with a space in the name). RUN_PYTHON
5+
expands to that path, so every command-position use has to be quoted. Unquoted,
6+
sh word-splits it and ensure-venv fails while blaming the interpreter:
7+
8+
/bin/sh: /Volumes/Some Drive/repo/.venv/bin/python: not found
9+
ERROR: OpenMontage requires Python 3.10+.
10+
Current interpreter is unavailable: /Volumes/Some Drive/repo/.venv/bin/python
11+
12+
The interpreter is fine. Every make target fails, and the reported cause is
13+
wrong, which sends people off to reinstall a Python they do not need.
14+
"""
15+
16+
import os
17+
import re
18+
import shutil
19+
import stat
20+
import subprocess
21+
import sys
22+
from pathlib import Path
23+
24+
import pytest
25+
26+
PROJECT_ROOT = Path(__file__).resolve().parent.parent.parent
27+
MAKEFILE = PROJECT_ROOT / "Makefile"
28+
29+
# Command position: the start of a recipe line (optionally after @), after a
30+
# shell operator, inside a command substitution, or in the PIP recipe variable.
31+
# Display-only uses inside echo strings do not need quoting.
32+
COMMAND_POSITION = re.compile(r"(?:^\t@?|\|\| |&& |\$\$\(|^PIP = )\$\(RUN_PYTHON\)", re.M)
33+
34+
35+
def test_run_python_is_quoted_in_every_command_position():
36+
text = MAKEFILE.read_text()
37+
offenders = []
38+
for match in COMMAND_POSITION.finditer(text):
39+
line_no = text.count("\n", 0, match.start()) + 1
40+
offenders.append(f" Makefile:{line_no}: {text.splitlines()[line_no - 1].strip()[:90]}")
41+
42+
assert not offenders, (
43+
"Unquoted $(RUN_PYTHON) in command position. The interpreter path can "
44+
"contain spaces, so these word-split in sh:\n" + "\n".join(offenders)
45+
)
46+
47+
48+
@pytest.mark.skipif(shutil.which("make") is None, reason="make is not installed")
49+
def test_ensure_venv_succeeds_when_the_venv_path_contains_spaces(tmp_path):
50+
"""End-to-end: run a real make target from a path with a space in it."""
51+
workdir = tmp_path / "dir with spaces"
52+
bindir = workdir / ".venv" / "bin"
53+
bindir.mkdir(parents=True)
54+
shutil.copy(MAKEFILE, workdir / "Makefile")
55+
56+
# A wrapper rather than a symlink, so `python -m pip` still resolves
57+
# against the real environment instead of this empty directory.
58+
fake_python = bindir / "python"
59+
fake_python.write_text(f'#!/bin/sh\nexec "{sys.executable}" "$@"\n')
60+
fake_python.chmod(fake_python.stat().st_mode | stat.S_IEXEC | stat.S_IXGRP | stat.S_IXOTH)
61+
62+
env = {**os.environ, "VIRTUAL_ENV": str(workdir / ".venv"), "CONDA_PREFIX": ""}
63+
proc = subprocess.run(
64+
["make", "ensure-venv"],
65+
cwd=workdir,
66+
env=env,
67+
capture_output=True,
68+
text=True,
69+
timeout=120,
70+
)
71+
72+
assert proc.returncode == 0, (
73+
f"make ensure-venv failed from a path containing spaces.\n"
74+
f"stdout:\n{proc.stdout}\nstderr:\n{proc.stderr}"
75+
)
76+
assert "No such file or directory" not in proc.stderr
77+
assert "requires Python" not in proc.stdout

0 commit comments

Comments
 (0)