11{% raw %} import argparse
22import os
33import platform
4- import shlex
54import shutil
65import subprocess
76import sys
@@ -15,9 +14,9 @@ COPIER_TEMPLATE_EXTENSIONS_VERSION = "{% endraw %}{{ copier_template_extensions_
1514PRE_COMMIT_VERSION = "{% endraw %} {{ pre_commit_version }}{% raw %} "
1615TASK_VERSION = "{% endraw %} {{ task_version }}{% raw %} "
1716DOWNLOAD_TIMEOUT_SECONDS = 90
18- # Where both uv's and Task's installers place binaries . Resolves from USERPROFILE on Windows, so it
19- # matches the runner's home directory without assuming its user name. Already on PATH on POSIX, but
20- # not on Windows, which is why uv is invoked through an absolute path there.
17+ # Where uv places both itself and the executables of the tools it installs . Resolves from USERPROFILE
18+ # on Windows, so it matches the runner's home directory without assuming its user name. Already on
19+ # PATH on POSIX, but not on Windows, which is why uv is invoked through an absolute path there.
2120LOCAL_BIN_DIR = Path.home() / ".local" / "bin"
2221INSTALL_SSM_PLUGIN_BY_DEFAULT = {% endraw %}{% if is_child_of_copier_base_template is not defined and install_aws_ssm_port_forwarding_plugin is defined and install_aws_ssm_port_forwarding_plugin is sameas (true ) %} True{% else %} False{% endif %}{% raw %}
2322# A floor rather than an exact pin: GitHub's runner images ship their own build of the plugin and only
@@ -60,55 +59,60 @@ def pwsh_cmd(cmd: str) -> list[str]:
6059 return [pwsh, "-NoProfile", "-NonInteractive", "-Command", cmd]
6160
6261
63- def install_task( *, is_windows: bool) -> None:
64- """Install the pinned Task release into `LOCAL_BIN_DIR`.
62+ def install_uv(uv_env: dict[str, str], *, is_windows: bool) -> None:
63+ """Install the pinned uv release into `LOCAL_BIN_DIR`.
6564
66- Deliberately not `npm install -g @go-task/cli`: that writes to the global prefix of whichever
67- node is on PATH, and in CI that is the pnpm-managed node installed by `pnpm/setup`, whose prefix
68- bin directory is not the one on PATH. The package installs successfully and the linked
69- executable is then unreachable, so `task` fails with exit status 127.
65+ Runs regardless of `--no-python`, because uv is also how Task is installed, and every job needs
66+ the task runner even when it has no Python environments to set up.
7067
71- Verification goes through the absolute path because this process resolved PATH before the
72- install; `GITHUB_PATH` is appended so later steps in the same CI job can invoke `task` by name .
68+ `uv_env` is mutated on Windows rather than copied: `LOCAL_BIN_DIR` is not on the runner's PATH
69+ there, and every later uv invocation needs it in front .
7370 """
74- LOCAL_BIN_DIR.mkdir(parents=True, exist_ok=True)
7571 if is_windows:
76- if platform.machine().lower() == "arm64":
77- windows_arch = "arm64"
78- else:
79- windows_arch = "amd64"
80- archive_url = (
81- f"https://github.com/go-task/task/releases/download/v{TASK_VERSION}/task_windows_{windows_arch}.zip"
82- )
83- # The bin directory is handed over as an environment variable rather than interpolated into the
84- # script text: PowerShell single quotes take no escapes, so an apostrophe in the home directory
85- # would terminate the string early. A variable reference is one argument whatever it contains.
86- task_env = dict(os.environ)
87- task_env.update({"TASK_BIN_DIR": str(LOCAL_BIN_DIR)})
88- powershell_statements = [
89- "$ErrorActionPreference = 'Stop'",
90- "$archive = Join-Path $env:TEMP 'task-release.zip'",
91- f"Invoke-WebRequest -UseBasicParsing -Uri '{archive_url}' -OutFile $archive",
92- "Expand-Archive -Path $archive -DestinationPath $env:TASK_BIN_DIR -Force",
93- "Remove-Item $archive",
94- ]
72+ uv_env.update({"PATH": rf"{LOCAL_BIN_DIR};{uv_env['PATH']}"})
9573 _ = subprocess.run( # noqa: S603 # this is all our own input
96- pwsh_cmd("; ".join(powershell_statements) ),
74+ pwsh_cmd(f"irm https://astral.sh/uv/{UV_VERSION}/install.ps1 | iex" ),
9775 check=True,
98- env=task_env ,
76+ env=uv_env ,
9977 timeout=DOWNLOAD_TIMEOUT_SECONDS,
10078 )
101- task_path = LOCAL_BIN_DIR / "task.exe"
10279 else:
103- # The installer resolves the pinned tag against the published checksums, so the archive is verified.
104- # The bin directory is shell-quoted because a space or apostrophe in the home directory would
105- # otherwise split it into multiple arguments, or unbalance the quoting outright.
10680 _ = subprocess.run( # noqa: S602 # we need to set shell to true to use the pipe operator, and this is all our own input
107- f"curl -fsSL --connect-timeout 20 --max-time 40 --retry 3 --retry-delay 5 --retry-connrefused --proto '=https' https://taskfile.dev/ install.sh | sh -s -- -b {shlex.quote(str(LOCAL_BIN_DIR))} v{TASK_VERSION} ",
81+ f"curl -fsSL --connect-timeout 20 --max-time 40 --retry 3 --retry-delay 5 --retry-connrefused --proto '=https' https://astral.sh/uv/{UV_VERSION}/ install.sh | sh",
10882 check=True,
10983 shell=True,
84+ env=uv_env,
11085 timeout=DOWNLOAD_TIMEOUT_SECONDS,
11186 )
87+ # TODO: add uv autocompletion to the shell https://docs.astral.sh/uv/getting-started/installation/#shell-autocompletion
88+
89+
90+ def install_task(uv_path: str, uv_env: dict[str, str], *, is_windows: bool) -> None:
91+ """Install the pinned Task release into `LOCAL_BIN_DIR` as a uv tool.
92+
93+ `go-task-bin` repackages the upstream release archives as one wheel per platform, so a single uv
94+ invocation covers Windows, macOS and Linux on both x86_64 and arm64. Task publishes nothing to
95+ PyPI itself, so this is knowingly a third-party distribution: the version is pinned and uv
96+ records the wheel hash in the tool receipt it writes.
97+
98+ Deliberately not `npm install -g @go-task/cli`: that writes to the global prefix of whichever
99+ node is on PATH, and in CI that is the pnpm-managed node installed by `pnpm/setup`, whose prefix
100+ bin directory is not the one on PATH. The package installs successfully and the linked
101+ executable is then unreachable, so `task` fails with exit status 127.
102+
103+ Verification goes through the absolute path because this process resolved PATH before the
104+ install; `GITHUB_PATH` is appended so later steps in the same CI job can invoke `task` by name.
105+ """
106+ LOCAL_BIN_DIR.mkdir(parents=True, exist_ok=True)
107+ _ = subprocess.run( # noqa: S603 # this is all our own input
108+ [uv_path, "tool", "install", f"go-task-bin=={TASK_VERSION}"],
109+ check=True,
110+ env=uv_env,
111+ timeout=DOWNLOAD_TIMEOUT_SECONDS,
112+ )
113+ if is_windows:
114+ task_path = LOCAL_BIN_DIR / "task.exe"
115+ else:
112116 task_path = LOCAL_BIN_DIR / "task"
113117 _ = subprocess.run([str(task_path), "--version"], check=True) # noqa: S603 # this is all our own input
114118 if "GITHUB_PATH" in os.environ:
@@ -241,24 +245,8 @@ def main():
241245 uv_path = str(LOCAL_BIN_DIR / "uv")
242246 else:
243247 uv_path = "uv"
248+ install_uv(uv_env, is_windows=is_windows)
244249 if not args.no_python:
245- if is_windows:
246- uv_env.update({"PATH": rf"{LOCAL_BIN_DIR};{uv_env['PATH']}"})
247- _ = subprocess.run( # noqa: S603 # this is all our own input
248- pwsh_cmd(f"irm https://astral.sh/uv/{UV_VERSION}/install.ps1 | iex"),
249- check=True,
250- env=uv_env,
251- timeout=DOWNLOAD_TIMEOUT_SECONDS,
252- )
253- else:
254- _ = subprocess.run( # noqa: S602 # we need to set shell to true to use the pipe operator, and this is all our own input
255- f"curl -fsSL --connect-timeout 20 --max-time 40 --retry 3 --retry-delay 5 --retry-connrefused --proto '=https' https://astral.sh/uv/{UV_VERSION}/install.sh | sh",
256- check=True,
257- shell=True,
258- env=uv_env,
259- timeout=DOWNLOAD_TIMEOUT_SECONDS,
260- )
261- # TODO: add uv autocompletion to the shell https://docs.astral.sh/uv/getting-started/installation/#shell-autocompletion
262250 _ = subprocess.run( # noqa: S603 # this is all our own input
263251 [
264252 uv_path,
@@ -283,19 +271,19 @@ def main():
283271 env=uv_env,
284272 timeout=DOWNLOAD_TIMEOUT_SECONDS,
285273 )
286- _ = subprocess.run( # noqa: S603 # this is all our own input
287- [
288- uv_path,
289- "tool",
290- "list",
291- ],
292- check=True,
293- env=uv_env,
294- )
295274 if not args.no_node:
296275 run_node_cmds(["npm -v", f"npm install -g pnpm@{PNPM_VERSION}", "pnpm -v"], is_windows=is_windows)
297276 # Task is installed outside the --no-node branch because CI always passes --no-node (pnpm/setup handles pnpm there), and every job still needs the task runner
298- install_task(is_windows=is_windows)
277+ install_task(uv_path, uv_env, is_windows=is_windows)
278+ _ = subprocess.run( # noqa: S603 # this is all our own input
279+ [
280+ uv_path,
281+ "tool",
282+ "list",
283+ ],
284+ check=True,
285+ env=uv_env,
286+ )
299287 if INSTALL_SSM_PLUGIN_BY_DEFAULT and not args.skip_installing_ssm_plugin:
300288 ensure_ssm_plugin(is_windows=is_windows)
301289
0 commit comments