Skip to content

Commit 2110e2b

Browse files
committed
fix(common): add pip to compile
1 parent 042705e commit 2110e2b

File tree

3 files changed

+92
-2
lines changed

3 files changed

+92
-2
lines changed

README.md

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,41 @@ The current cloud foundry [python-buildpack](https://github.com/cloudfoundry/pyt
66

77
## How to use
88

9+
10+
### Defining the initialization script
11+
12+
This buildpack detects uv-managed apps when both `pyproject.toml` and `uv.lock` are present.
13+
14+
At staging time, `bin/compile` exports the locked dependencies from `uv.lock`, installs them into `.python_packages`, and writes a `.profile.d/python.sh` file so the app can import those staged packages at runtime. If the app uses a `src/` layout, that `src/` directory is also added to `PYTHONPATH`.
15+
16+
At release time, `bin/release` chooses the web command in this order:
17+
18+
1. If the app has a `Procfile`, the buildpack does not generate a default process type and Cloud Foundry uses the `Procfile`.
19+
2. If `pyproject.toml` exists and `[project.scripts]` contains a script named exactly the same as `[project].name`, that script is used.
20+
3. Otherwise, if `[project.scripts]` contains a `start` script, that script is used.
21+
4. Otherwise, if `main.py` exists, the buildpack uses `python3 main.py`.
22+
5. Otherwise, if `app.py` exists, the buildpack uses `python3 app.py`.
23+
6. If none of the above are present, the buildpack emits an empty `web` process and you must provide your own entrypoint.
24+
25+
For `pyproject.toml` scripts, the buildpack converts a console-script target such as:
26+
27+
```toml
28+
[project]
29+
name = "my-app"
30+
31+
[project.scripts]
32+
my-app = "server.main:run"
33+
start = "server.main:start"
34+
```
35+
36+
into a process command like:
37+
38+
```sh
39+
python3 -c "from server.main import run; run()"
40+
```
41+
42+
In the example above, `my-app` wins over `start` because it matches `[project].name`.
43+
944
## Testing Locally
1045

1146
Run the buildpack test flow from the repository root:

bin/compile

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,17 @@ else
1717
exit 1
1818
fi
1919

20+
ensure_pip() {
21+
# Some CF Python runtimes do not expose pip by default, so bootstrap it
22+
# before the buildpack tries to install uv or exported dependencies.
23+
if "$PYTHON_BIN" -m pip --version >/dev/null 2>&1; then
24+
return 0
25+
fi
26+
27+
echo "pip not found for $PYTHON_BIN. Bootstrapping with ensurepip."
28+
"$PYTHON_BIN" -m ensurepip --upgrade
29+
}
30+
2031
cd "$BUILD_DIR"
2132

2233
# Only treat the app as a supported uv project when both the project
@@ -30,10 +41,14 @@ if [ -f "pyproject.toml" ] && [ -f "uv.lock" ]; then
3041

3142
mkdir -p "$PIP_CACHE_DIR" "$UV_CACHE_DIR"
3243

44+
# Install uv into the selected interpreter so the buildpack can export the
45+
# exact locked dependency set from the app's uv.lock file.
46+
ensure_pip
3347
"$PYTHON_BIN" -m pip install uv
3448

3549
PYTHON_VERSION=$("$PYTHON_BIN" -c 'import sys; print(f"{sys.version_info.major}.{sys.version_info.minor}")')
36-
# Stage dependencies into a buildpack-owned location instead of a local venv.
50+
# Stage dependencies into a buildpack-owned location instead of a local venv
51+
# because CF launches the app from the droplet, not from a project virtualenv.
3752
SITE_PACKAGES_DIR="$BUILD_DIR/.python_packages/lib/python${PYTHON_VERSION}/site-packages"
3853
SRC_DIR="$BUILD_DIR/src"
3954
PROFILE_DIR="$BUILD_DIR/.profile.d"

test/unit/compile_test.sh

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ assert_path_exists() {
6767
}
6868

6969
setup_fake_commands() {
70+
local pip_available="${1:-1}"
7071
mkdir -p "$FAKE_BIN_DIR"
7172

7273
cat > "$FAKE_BIN_DIR/python3" <<'EOF'
@@ -78,6 +79,23 @@ if [ "$#" -ge 2 ] && [ "$1" = "-c" ] && [ "$2" = "import sys; print(f\"{sys.vers
7879
exit 0
7980
fi
8081
82+
if [ "$#" -ge 3 ] && [ "$1" = "-m" ] && [ "$2" = "pip" ] && [ "$3" = "--version" ]; then
83+
if [ "${FAKE_PIP_AVAILABLE:-1}" = "1" ]; then
84+
printf 'pip 24.0 from /fake/site-packages/pip (python 3.13)\n'
85+
exit 0
86+
fi
87+
88+
echo "No module named pip" >&2
89+
exit 1
90+
fi
91+
92+
if [ "$#" -ge 3 ] && [ "$1" = "-m" ] && [ "$2" = "ensurepip" ] && [ "$3" = "--upgrade" ]; then
93+
log_file="${TEST_ROOT}/ensurepip.log"
94+
mkdir -p "$(dirname "$log_file")"
95+
printf '%s\n' "$*" >> "$log_file"
96+
exit 0
97+
fi
98+
8199
if [ "$#" -ge 3 ] && [ "$1" = "-m" ] && [ "$2" = "pip" ]; then
82100
log_file="${TEST_ROOT}/pip.log"
83101
mkdir -p "$(dirname "$log_file")"
@@ -149,6 +167,7 @@ exit 1
149167
EOF
150168

151169
chmod +x "$FAKE_BIN_DIR/python3" "$FAKE_BIN_DIR/uv"
170+
FAKE_PIP_AVAILABLE="$pip_available"
152171
}
153172

154173
run_compile() {
@@ -158,7 +177,7 @@ run_compile() {
158177
local output_file="$TEST_ROOT/output.txt"
159178

160179
set +e
161-
PATH="$FAKE_BIN_DIR:/usr/bin:/bin" TEST_ROOT="$TEST_ROOT" "$COMPILE_SCRIPT" "$build_dir" "$cache_dir" "$env_dir" >"$output_file" 2>&1
180+
PATH="$FAKE_BIN_DIR:/usr/bin:/bin" TEST_ROOT="$TEST_ROOT" FAKE_PIP_AVAILABLE="${FAKE_PIP_AVAILABLE:-1}" "$COMPILE_SCRIPT" "$build_dir" "$cache_dir" "$env_dir" >"$output_file" 2>&1
162181
status=$?
163182
set -e
164183

@@ -193,6 +212,26 @@ test_compile_succeeds_for_locked_uv_project() {
193212
assert_file_contains "$TEST_ROOT/pip.log" "--no-deps --target $site_packages_dir -r $export_file" "compile should install exported dependencies into the staged site-packages directory"
194213
}
195214

215+
test_compile_bootstraps_pip_when_missing() {
216+
# Arrange
217+
local build_dir="$TEST_ROOT/missing-pip/build"
218+
local cache_dir="$TEST_ROOT/missing-pip/cache"
219+
local env_dir="$TEST_ROOT/missing-pip/env"
220+
221+
mkdir -p "$build_dir" "$cache_dir" "$env_dir"
222+
touch "$build_dir/pyproject.toml" "$build_dir/uv.lock"
223+
setup_fake_commands 0
224+
225+
# Act
226+
run_compile "$build_dir" "$cache_dir" "$env_dir"
227+
228+
# Assert
229+
assert_exit_code "$status" 0 "compile should bootstrap pip when the interpreter does not ship it"
230+
assert_contains "$output" "pip not found for python3. Bootstrapping with ensurepip." "compile should explain when it needs to bootstrap pip"
231+
assert_file_contains "$TEST_ROOT/ensurepip.log" "-m ensurepip --upgrade" "compile should bootstrap pip before using it"
232+
assert_file_contains "$TEST_ROOT/pip.log" "-m pip install uv" "compile should continue with pip installs after bootstrapping"
233+
}
234+
196235
test_compile_adds_src_directory_to_pythonpath_when_present() {
197236
# Arrange
198237
local build_dir="$TEST_ROOT/src-layout/build"
@@ -231,6 +270,7 @@ test_compile_fails_when_lockfile_is_missing() {
231270
}
232271

233272
test_compile_succeeds_for_locked_uv_project
273+
test_compile_bootstraps_pip_when_missing
234274
test_compile_adds_src_directory_to_pythonpath_when_present
235275
test_compile_fails_when_lockfile_is_missing
236276

0 commit comments

Comments
 (0)