-
Notifications
You must be signed in to change notification settings - Fork 1
191 lines (167 loc) · 8.21 KB
/
Copy pathbuild.python.yml
File metadata and controls
191 lines (167 loc) · 8.21 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
name: Build Python project
on:
workflow_call:
inputs:
python_version:
required: false
type: string
description: "What Python version to use."
default: "3.13"
working-directory:
required: false
type: string
default: "."
artifact-name:
required: false
type: string
default: "python-artifact"
git-sha:
required: false
type: string
default: ${{ github.event.pull_request.head.sha || github.sha }}
runs-on:
required: false
type: string
default: "ubuntu-latest"
uv-workspace:
required: false
type: boolean
default: false
description: >-
Set to true if `working-directory` is a member of a uv workspace
(i.e. it has local path dependencies on sibling packages declared via
`tool.uv.sources` with `workspace = true`, such as other packages
under a monorepo's `libs/` directory). When enabled, all workspace
members are built together and the package's local (workspace)
dependencies are installed from the resulting wheels instead of
being resolved from a package index. This avoids failures for
private, unpublished packages, and avoids name collisions with
unrelated public packages that happen to share the same name.
Requires the uv workspace root (the `pyproject.toml`/`uv.lock`
declaring `[tool.uv.workspace]`) to be at the repository root.
python-platform:
required: false
type: string
default: ""
description: >-
Optional target platform to install dependencies for, passed to
`uv pip install --python-platform`. Useful when the build runner's
architecture differs from the deployment target's, e.g.
"aarch64-manylinux_2_28" when building on an x86_64 runner for an
arm64 Lambda.
outputs:
artifact-name:
description: The name of the artifact generated by the build
value: ${{ jobs.build.outputs.artifact-name }}
jobs:
build:
name: Build Python
runs-on: ${{ inputs.runs-on }}
outputs:
artifact-name: ${{ steps.artifact-name.outputs.artifact-name }}
steps:
- uses: actions/checkout@v6
with:
fetch-depth: '0' # Fetch all history for all branches and tags (for branches)
ref: ${{ inputs.git-sha }}
- name: Install uv
uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
with:
enable-cache: true
cache-dependency-glob: "${{ inputs.uv-workspace && 'uv.lock' || format('{0}/uv.lock', inputs.working-directory) }}"
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6
with:
python-version: ${{ inputs.python_version }}
- name: Install dependencies
if: ${{ !inputs.uv-workspace }}
working-directory: ${{ inputs.working-directory }}
run: |
uv build
uv pip install dist/*.whl --target ./out ${{ inputs.python-platform != '' && format('--python-platform {0}', inputs.python-platform) || '' }}
find ./out/ -name "__pycache__" -type d -exec rm -rf {} \;
- name: Install dependencies (uv workspace)
if: ${{ inputs.uv-workspace }}
env:
WORKING_DIRECTORY: ${{ inputs.working-directory }}
PYTHON_PLATFORM: ${{ inputs.python-platform }}
run: |
set -euo pipefail
if ! python3 -c "import sys; sys.exit(0 if sys.version_info >= (3, 11) else 1)"; then
echo "uv-workspace: true requires python_version >= 3.11 (tomllib is stdlib only from 3.11)." >&2
exit 1
fi
# `uv sync`/`uv build --all-packages` operate on the uv workspace
# found from the current directory upwards, and we always run them
# from the repo root (see below) - so the workspace root must be
# the repo root itself, not some nested directory.
if [ ! -f "uv.lock" ] || ! grep -q '^\[tool\.uv\.workspace\]' pyproject.toml 2>/dev/null; then
echo "uv-workspace: true requires the uv workspace root (pyproject.toml with [tool.uv.workspace], and uv.lock) to be at the repository root." >&2
exit 1
fi
# `uv build`/`uv export` for a workspace member resolve the workspace
# root themselves, but write relative output (e.g. `dist/`) relative
# to the current directory, so we always run from the repo root to
# get a single, predictable location for every workspace member's
# wheel.
PKG_NAME="$(python3 -c "import tomllib; print(tomllib.load(open('${WORKING_DIRECTORY}/pyproject.toml','rb'))['project']['name'])")"
# Creates the uv environment without updating or changing uv.lock
# Build every workspace member once, so local (path) dependencies of
# this package are available as real wheels alongside it.
uv sync --frozen
uv build --all-packages --wheel
# Pin every *third-party* dependency to the exact resolved version
# from the lockfile. Workspace members are intentionally excluded
# here (`--no-emit-workspace`) - they are installed explicitly from
# local wheels below instead, since they aren't published to any
# index and their names could otherwise collide with unrelated
# public packages.
uv export --frozen --package "$PKG_NAME" --no-emit-workspace --no-dev --no-hashes -o /tmp/uv-workspace-constraints.txt
# Discover which sibling workspace packages this package actually
# depends on (directly or transitively), so we install exactly the
# matching local wheels and nothing else. Note: `uv export` also
# emits an `-e <path>` entry for $PKG_NAME itself (not just its
# workspace dependencies), so the loop below picks up and installs
# its own freshly-built wheel too - there's no separate, explicit
# install of $PKG_NAME's wheel anywhere in this script.
uv export --frozen --package "$PKG_NAME" --no-dev --no-hashes -o /tmp/uv-workspace-full-export.txt
LOCAL_WHEELS=()
while IFS= read -r local_dir; do
[ -z "$local_dir" ] && continue
local_name="$(python3 -c "import tomllib; print(tomllib.load(open('${local_dir}/pyproject.toml','rb'))['project']['name'])")"
# Wheel filenames use the normalized distribution name: lowercased,
# with runs of `-`, `_` and `.` collapsed to a single `_`.
normalized_name="$(echo "$local_name" | tr '[:upper:]' '[:lower:]' | sed -E 's/[-_.]+/_/g')"
wheel="$(find dist -maxdepth 1 -type f -name "${normalized_name}-*.whl" -print -quit)"
if [ -z "$wheel" ]; then
echo "No wheel found for workspace package ${local_name}" >&2
exit 1
fi
LOCAL_WHEELS+=("$wheel")
done < <(awk '$1 == "-e" { print $2 }' /tmp/uv-workspace-full-export.txt)
mkdir -p "${WORKING_DIRECTORY}/out"
INSTALL_ARGS=(-c /tmp/uv-workspace-constraints.txt --target "${WORKING_DIRECTORY}/out")
if [ -n "$PYTHON_PLATFORM" ]; then
INSTALL_ARGS+=(--python-platform "$PYTHON_PLATFORM")
fi
uv pip install "${INSTALL_ARGS[@]}" "${LOCAL_WHEELS[@]}"
find "${WORKING_DIRECTORY}/out" -name "__pycache__" -type d -exec rm -rf {} \;
- name: Validate and build artifact name
id: artifact-name
run: |
NAME="$INPUT_NAME"
NAME="$(echo "$NAME" | xargs)"
if [ -z "$NAME" ]; then
echo "Error: artifact-name cannot be empty or whitespace"
exit 1
fi
ARTIFACT_NAME="${{ inputs.working-directory }}-$NAME"
ARTIFACT_NAME="${ARTIFACT_NAME//\//-}"
echo "artifact-name=$ARTIFACT_NAME" >> $GITHUB_OUTPUT
env:
INPUT_NAME: ${{ inputs.artifact-name }}
- name: Store build as Artifact
uses: actions/upload-artifact@v6
with:
name: ${{ steps.artifact-name.outputs.artifact-name }}
path: ${{ inputs.working-directory }}/out
retention-days: 5