Install node, yarn and python from the distribution (closes #146)
check / check (push) Canceled after 0s
check / check (push) Canceled after 0s
Follows sneak's ruling at #146 (comment). Runtimes and the installer tools bundled with python come from the distribution, with no version and no hash. Third-party packages stay pinned by hash. script/bootstrap drops nvm and corepack. It installs node, yarn and the node packages only when yarn.lock is in the tree, and stops there when yarn's major version is not 1. This repository's Dockerfile starts from Alpine 3.24. The Python example starts from debian:trixie-slim, keeps pipenv at $HOME/.pipenv on a host and in an image, and sets PIP_ONLY_BINARY=:all:, so pip downloads no build tools without a hash check. Model: opus-5-5
This commit is contained in:
+16
-10
@@ -2,11 +2,15 @@
|
|||||||
# lint` or `script/lint`, which are themselves a docker build and would
|
# lint` or `script/lint`, which are themselves a docker build and would
|
||||||
# recurse into a daemon that does not exist in a build step.
|
# recurse into a daemon that does not exist in a build step.
|
||||||
#
|
#
|
||||||
# Every stage declares ARG CACHEBUST first: script/ passes a new value on
|
# In every stage, ARG CACHEBUST comes right after the step that installs
|
||||||
# every build, which reruns every RUN step after it.
|
# system packages. script/ passes a new value on every build. That value
|
||||||
|
# reruns every RUN step below the ARG line, and the install step keeps
|
||||||
|
# its layer. node and yarn come from the distribution, which is trusted,
|
||||||
|
# so they need no version and no hash.
|
||||||
#
|
#
|
||||||
# node 22-alpine, 2026-02-22
|
# alpine 3.24.3, 2026-11-04
|
||||||
FROM node@sha256:e4bf2a82ad0a4037d28035ae71529873c069b13eb0455466ae0bc13363826e34 AS lint
|
FROM alpine@sha256:be2656061b536c1883c4034790e21d0a09c5aaaf51968b89368a3c0371ea2c9a AS lint
|
||||||
|
RUN apk add --no-cache git make nodejs yarn
|
||||||
ARG CACHEBUST
|
ARG CACHEBUST
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
@@ -21,8 +25,9 @@ RUN yarn run prettier --check '**/*.md' --tab-width 4 --prose-wrap always
|
|||||||
|
|
||||||
# Test phase, same shape and for the same reason.
|
# Test phase, same shape and for the same reason.
|
||||||
#
|
#
|
||||||
# node 22-alpine, 2026-02-22
|
# alpine 3.24.3, 2026-11-04
|
||||||
FROM node@sha256:e4bf2a82ad0a4037d28035ae71529873c069b13eb0455466ae0bc13363826e34 AS test
|
FROM alpine@sha256:be2656061b536c1883c4034790e21d0a09c5aaaf51968b89368a3c0371ea2c9a AS test
|
||||||
|
RUN apk add --no-cache git make nodejs yarn
|
||||||
ARG CACHEBUST
|
ARG CACHEBUST
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
@@ -41,8 +46,9 @@ RUN echo "No tests defined."
|
|||||||
# this image cannot be produced unless lint and test passed. A stage
|
# this image cannot be produced unless lint and test passed. A stage
|
||||||
# appended after this one would drop all three out of a plain build.
|
# appended after this one would drop all three out of a plain build.
|
||||||
#
|
#
|
||||||
# node 22-alpine, 2026-02-22
|
# alpine 3.24.3, 2026-11-04
|
||||||
FROM node@sha256:e4bf2a82ad0a4037d28035ae71529873c069b13eb0455466ae0bc13363826e34
|
FROM alpine@sha256:be2656061b536c1883c4034790e21d0a09c5aaaf51968b89368a3c0371ea2c9a
|
||||||
|
RUN apk add --no-cache git make nodejs yarn
|
||||||
ARG CACHEBUST
|
ARG CACHEBUST
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
@@ -50,8 +56,8 @@ WORKDIR /app
|
|||||||
COPY --from=lint /app/package.json /dev/null
|
COPY --from=lint /app/package.json /dev/null
|
||||||
COPY --from=test /app/package.json /dev/null
|
COPY --from=test /app/package.json /dev/null
|
||||||
|
|
||||||
# script/bootstrap installs all prerequisites. Manifests are copied
|
# script/bootstrap finds the system packages above and installs the node
|
||||||
# first so that layer stays cached until dependencies change.
|
# packages.
|
||||||
COPY script/ script/
|
COPY script/ script/
|
||||||
COPY package.json yarn.lock ./
|
COPY package.json yarn.lock ./
|
||||||
RUN script/bootstrap
|
RUN script/bootstrap
|
||||||
|
|||||||
@@ -156,9 +156,9 @@ A stage with none of these steps declares `ARG CACHEBUST` right after `FROM`.
|
|||||||
The `script/` builds pass a new `CACHEBUST` value on every build, which reruns
|
The `script/` builds pass a new `CACHEBUST` value on every build, which reruns
|
||||||
every `RUN` step after that line and leaves the layers above it and the cache
|
every `RUN` step after that line and leaves the layers above it and the cache
|
||||||
mounts in place; `--no-cache` would build those layers again and empty the
|
mounts in place; `--no-cache` would build those layers again and empty the
|
||||||
mounts. This repo's own `Dockerfile` declares `ARG CACHEBUST` right after `FROM`
|
mounts. This repo's own `Dockerfile` installs git, make, node and yarn right
|
||||||
in every stage: each stage installs system packages and node packages together
|
after `FROM` in every stage, above `ARG CACHEBUST`. `script/bootstrap` installs
|
||||||
in one `script/bootstrap` step, and check builds may keep system packages but
|
the node packages below that line, because check builds keep system packages but
|
||||||
not node packages.
|
not node packages.
|
||||||
|
|
||||||
## Rationale
|
## Rationale
|
||||||
|
|||||||
@@ -21,6 +21,23 @@ fmt-check, and commit.
|
|||||||
|
|
||||||
# Completed Steps
|
# Completed Steps
|
||||||
|
|
||||||
|
- 2026-11-04: node, yarn, npm, python, pip and venv now come from the
|
||||||
|
distribution with no version and no hash
|
||||||
|
(https://git.eeqj.de/sneak/prompts/issues/146), on sneak's ruling at
|
||||||
|
https://git.eeqj.de/sneak/prompts/issues/146#issuecomment-166033. Third-party
|
||||||
|
packages stay pinned by hash. These changed:
|
||||||
|
- `script/bootstrap` no longer uses nvm or corepack, and stops when yarn's
|
||||||
|
major version is not 1.
|
||||||
|
- `script/bootstrap` installs node, yarn and the node packages only when
|
||||||
|
`yarn.lock` is in the tree.
|
||||||
|
- `script/bootstrap`, `script/fmt` and `script/fmt-check` run `yarn` or
|
||||||
|
`yarnpkg`, whichever is present.
|
||||||
|
- This repository's `Dockerfile` starts from Alpine 3.24.
|
||||||
|
- The Python `test` phase example starts from `debian:trixie-slim`.
|
||||||
|
- A Python repository's `script/bootstrap` creates its virtual environment
|
||||||
|
at `$HOME/.pipenv`, so it works on a host and in a `Dockerfile`.
|
||||||
|
- `PIP_ONLY_BINARY=:all:` stops a package with no wheel, so pip downloads no
|
||||||
|
build tools without a hash check.
|
||||||
- 2026-11-04: Rewrote `prompts/REPO_POLICIES.md` sentence by sentence
|
- 2026-11-04: Rewrote `prompts/REPO_POLICIES.md` sentence by sentence
|
||||||
(https://git.eeqj.de/sneak/prompts/issues/136), on sneak's yes at
|
(https://git.eeqj.de/sneak/prompts/issues/136), on sneak's yes at
|
||||||
https://git.eeqj.de/sneak/prompts/issues/136#issuecomment-165193. Every rule
|
https://git.eeqj.de/sneak/prompts/issues/136#issuecomment-165193. Every rule
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
title: Code Styleguide — JavaScript
|
title: Code Styleguide — JavaScript
|
||||||
last_modified: 2026-02-22
|
last_modified: 2026-11-04
|
||||||
---
|
---
|
||||||
|
|
||||||
1. Use `const` for everything. If you need to reassign, use `let`. Never use
|
1. Use `const` for everything. If you need to reassign, use `let`. Never use
|
||||||
@@ -8,7 +8,8 @@ last_modified: 2026-02-22
|
|||||||
|
|
||||||
1. Use yarn for package management, avoid using npm.
|
1. Use yarn for package management, avoid using npm.
|
||||||
|
|
||||||
1. Use LTS node versions.
|
1. Install node from the distribution. If its version is too old, use a newer
|
||||||
|
distribution.
|
||||||
|
|
||||||
1. Use `prettier` for code formatting, with four spaces for indentation.
|
1. Use `prettier` for code formatting, with four spaces for indentation.
|
||||||
|
|
||||||
|
|||||||
@@ -81,11 +81,17 @@ with your task.
|
|||||||
- pipenv and its dependencies
|
- pipenv and its dependencies
|
||||||
- the packages from `Pipfile.lock`
|
- the packages from `Pipfile.lock`
|
||||||
|
|
||||||
It installs pipenv and its dependencies by hash, with
|
It creates a virtual environment at `$HOME/.pipenv` with `python3 -m venv`
|
||||||
|
when it is missing, then uses that environment's `pip` and `pipenv`. On a
|
||||||
|
host, it first installs the distribution's python and venv packages when
|
||||||
|
they are missing. It installs pipenv and its dependencies by hash, with
|
||||||
`pip install --require-hashes`. It installs the packages from `Pipfile.lock`
|
`pip install --require-hashes`. It installs the packages from `Pipfile.lock`
|
||||||
with `pipenv install --deploy`, plus `--dev` when the tests need the
|
with `pipenv install --deploy`, plus `--dev` when the tests need the
|
||||||
development packages. It installs no Python there, because the tests use the
|
development packages. It sets `PIP_ONLY_BINARY=:all:` for both installs, so
|
||||||
base image's. The tests run after `ARG CACHEBUST`, and every other stage
|
a package with no wheel for the image stops the install. In that step it
|
||||||
|
installs no Python, because a step above it already did. `Pipfile` names no
|
||||||
|
`python_version`, because `--deploy` stops when the distribution's Python
|
||||||
|
differs from it. The tests run after `ARG CACHEBUST`, and every other stage
|
||||||
installs its Python packages after it, as in the Python `test` phase example
|
installs its Python packages after it, as in the Python `test` phase example
|
||||||
in `REPO_POLICIES.md`
|
in `REPO_POLICIES.md`
|
||||||
|
|
||||||
@@ -196,13 +202,17 @@ with your task.
|
|||||||
`docker build` in `script/`. Without the bootstrap the CI run dies in
|
`docker build` in `script/`. Without the bootstrap the CI run dies in
|
||||||
`script/fmt-check`, which runs the formatter on the host and finds nothing
|
`script/fmt-check`, which runs the formatter on the host and finds nothing
|
||||||
installed.
|
installed.
|
||||||
- [ ] `script/fmt` and `script/fmt-check` pick their `yarn` with the same test
|
- [ ] `script/bootstrap` installs node, yarn and the node packages only when
|
||||||
as `script/bootstrap`: the `yarn` on `PATH` when the node on `PATH` has
|
`yarn.lock` is in the tree. node and yarn come from the distribution when
|
||||||
the pinned major version, and otherwise yarn under the pinned node, with
|
they are missing. With `yarn.lock` in the tree, `script/bootstrap` stops
|
||||||
nvm loaded through `$HOME/.nvm/nvm.sh`. `script/bootstrap` leaves the node
|
with a one-line message when yarn's major version is not 1.
|
||||||
and yarn it installs under nvm off the `PATH` of the shell that called it,
|
- [ ] A `Dockerfile` stage copies `yarn.lock` before it runs `script/bootstrap`
|
||||||
so a bare `yarn` exits 127 on a runner carrying nothing but docker and
|
only if that stage needs yarn. A stage that needs yarn starts from an
|
||||||
git, or runs under another node.
|
image whose distribution packages yarn 1, such as Alpine.
|
||||||
|
- [ ] `script/bootstrap`, `script/fmt` and `script/fmt-check` run yarn by
|
||||||
|
whichever name is present: `yarn`, or `yarnpkg` on Debian and Ubuntu. With
|
||||||
|
neither name on `PATH`, they print one line saying to run
|
||||||
|
`script/bootstrap`, and exit 1.
|
||||||
- [ ] `script/bootstrap` installs no linter of its own — delete the block, its
|
- [ ] `script/bootstrap` installs no linter of its own — delete the block, its
|
||||||
version variables and its call site. A JS repo's `yarn install` stays; it
|
version variables and its call site. A JS repo's `yarn install` stays; it
|
||||||
brings a linter along with every other dependency, and no verdict is taken
|
brings a linter along with every other dependency, and no verdict is taken
|
||||||
@@ -230,8 +240,12 @@ with your task.
|
|||||||
is a package from the distribution's own package index, which needs no
|
is a package from the distribution's own package index, which needs no
|
||||||
version and no hash. Pulling that index is also allowed. In a
|
version and no hash. Pulling that index is also allowed. In a
|
||||||
`Dockerfile`, the distribution is the pinned base image's. On a host, it
|
`Dockerfile`, the distribution is the pinned base image's. On a host, it
|
||||||
is the host's. A package source that a `Dockerfile` adds itself does not
|
is the host's, and the host's package manager stands for it, nix and brew
|
||||||
get this exception
|
included. A package source that a `Dockerfile` adds itself does not get
|
||||||
|
this exception
|
||||||
|
- [ ] node, yarn, npm, python, pip and venv come from the distribution wherever
|
||||||
|
possible, never from nvm, pyenv, corepack or `npm install -g yarn`. If the
|
||||||
|
distribution's version is too old, use a newer distribution
|
||||||
- [ ] Using `yarn`, not `npm` (JS projects)
|
- [ ] Using `yarn`, not `npm` (JS projects)
|
||||||
|
|
||||||
# Directory Structure
|
# Directory Structure
|
||||||
@@ -260,11 +274,12 @@ with your task.
|
|||||||
# Final
|
# Final
|
||||||
|
|
||||||
- [ ] `make check` passes
|
- [ ] `make check` passes
|
||||||
- [ ] `script/cibuild` succeeds in a fresh clone on a host carrying what CI has
|
- [ ] `script/cibuild` succeeds in a fresh clone on a host carrying what CI has.
|
||||||
(the runner image `docker.gitea.com/runner-images:ubuntu-latest`: docker,
|
That host is the runner image
|
||||||
git and node 24, with no yarn on `PATH`), and demonstrably executed the
|
`docker.gitea.com/runner-images:ubuntu-latest`, which has docker, git and
|
||||||
checks — a sub-second build, or `CACHED` on a step that lints or tests,
|
node 24. Take any yarn off `PATH` first. The run must show that the checks
|
||||||
one of the `RUN` steps below `ARG CACHEBUST`, means nothing ran. Above
|
executed. A sub-second build means nothing ran. So does `CACHED` on a step
|
||||||
|
that lints or tests, one of the `RUN` steps below `ARG CACHEBUST`. Above
|
||||||
that line, these steps are expected to show `CACHED`:
|
that line, these steps are expected to show `CACHED`:
|
||||||
- the steps that install system packages
|
- the steps that install system packages
|
||||||
- the steps that download Go's modules
|
- the steps that download Go's modules
|
||||||
|
|||||||
@@ -148,13 +148,20 @@ Template files can be fetched from:
|
|||||||
- pipenv and its dependencies
|
- pipenv and its dependencies
|
||||||
- the packages from `Pipfile.lock`
|
- the packages from `Pipfile.lock`
|
||||||
|
|
||||||
It installs pipenv and its dependencies by hash, with
|
It creates a virtual environment at `$HOME/.pipenv` with
|
||||||
`pip install --require-hashes`. It installs the packages from
|
`python3 -m venv` when it is missing, then uses that environment's `pip`
|
||||||
`Pipfile.lock` with `pipenv install --deploy`, plus `--dev` when the
|
and `pipenv`. On a host, it first installs the distribution's python and
|
||||||
tests need the development packages. It installs no Python there,
|
venv packages when they are missing. It installs pipenv and its
|
||||||
because the tests use the base image's. The tests run after
|
dependencies by hash, with `pip install --require-hashes`. It installs
|
||||||
`ARG CACHEBUST`, and every other stage installs its Python packages
|
the packages from `Pipfile.lock` with `pipenv install --deploy`, plus
|
||||||
after it, as in the Python `test` phase example in `REPO_POLICIES.md`
|
`--dev` when the tests need the development packages. It sets
|
||||||
|
`PIP_ONLY_BINARY=:all:` for both installs, so a package with no wheel
|
||||||
|
for the image stops the install. In that step it installs no Python,
|
||||||
|
because a step above it already did. `Pipfile` names no
|
||||||
|
`python_version`, because `--deploy` stops when the distribution's
|
||||||
|
Python differs from it. The tests run after `ARG CACHEBUST`, and every
|
||||||
|
other stage installs its Python packages after it, as in the Python
|
||||||
|
`test` phase example in `REPO_POLICIES.md`
|
||||||
|
|
||||||
- Server: the final stage builds and runs the application
|
- Server: the final stage builds and runs the application
|
||||||
- Non-server: the final stage brings up the dev environment
|
- Non-server: the final stage brings up the dev environment
|
||||||
@@ -183,14 +190,21 @@ are thin shims calling them. Model scripts:
|
|||||||
|
|
||||||
- [ ] scripts are POSIX sh (`#!/bin/sh`, `set -eu`, no bashisms) so they run on
|
- [ ] scripts are POSIX sh (`#!/bin/sh`, `set -eu`, no bashisms) so they run on
|
||||||
alpine images without bash
|
alpine images without bash
|
||||||
- [ ] `script/bootstrap` / `make bootstrap` — installs all dependencies,
|
- [ ] `script/bootstrap` / `make bootstrap` installs all dependencies,
|
||||||
idempotently, assuming nothing (pkg manager detection nix/apt/brew/apk;
|
idempotently, and assumes nothing is present:
|
||||||
node used if its major version is the pinned one, else pinned version via
|
- It detects the package manager: nix, apt, brew or apk.
|
||||||
nvm from a hash-verified archive, with yarn and the packages installed
|
- It installs node, yarn and the node packages only when `yarn.lock` is in
|
||||||
under it; pinned yarn via corepack); a non-server repo's development
|
the tree. node and yarn come from that package manager when they are
|
||||||
environment stage runs it instead of inline installs; a gate phase or the
|
missing.
|
||||||
build stage installs what its base image lacks either inline or by running
|
- With `yarn.lock` in the tree, it stops with a one-line message when yarn's
|
||||||
it
|
major version is not 1.
|
||||||
|
- A non-server repo's development environment stage runs it, with only
|
||||||
|
system packages installed inline above it.
|
||||||
|
- A gate phase or the build stage installs what its base image lacks, either
|
||||||
|
inline or by running it.
|
||||||
|
- [ ] A `Dockerfile` stage copies `yarn.lock` before it runs `script/bootstrap`
|
||||||
|
only if that stage needs yarn. A stage that needs yarn starts from an
|
||||||
|
image whose distribution packages yarn 1, such as Alpine.
|
||||||
- [ ] `script/setup` / `make setup` — readies a fresh clone: runs `bootstrap`,
|
- [ ] `script/setup` / `make setup` — readies a fresh clone: runs `bootstrap`,
|
||||||
then `install-precommit`, plus repo-specific init
|
then `install-precommit`, plus repo-specific init
|
||||||
- [ ] `script/test` / `make test` —
|
- [ ] `script/test` / `make test` —
|
||||||
@@ -220,13 +234,10 @@ are thin shims calling them. Model scripts:
|
|||||||
from `script/projectname`, each assigned on its own line before the build.
|
from `script/projectname`, each assigned on its own line before the build.
|
||||||
The bootstrap is required: CI checks out and runs this alone, and
|
The bootstrap is required: CI checks out and runs this alone, and
|
||||||
`script/fmt-check` runs the formatter on the host.
|
`script/fmt-check` runs the formatter on the host.
|
||||||
- [ ] `script/fmt` and `script/fmt-check` pick their `yarn` with the same test
|
- [ ] `script/bootstrap`, `script/fmt` and `script/fmt-check` run yarn by
|
||||||
as `script/bootstrap`: the `yarn` on `PATH` when the node on `PATH` has
|
whichever name is present: `yarn`, or `yarnpkg` on Debian and Ubuntu. With
|
||||||
the pinned major version, and otherwise yarn under the pinned node, with
|
neither name on `PATH`, they print one line saying to run
|
||||||
nvm loaded through `$HOME/.nvm/nvm.sh`. `script/bootstrap` leaves the node
|
`script/bootstrap`, and exit 1.
|
||||||
and yarn it installs under nvm off the `PATH` of the shell that called it,
|
|
||||||
so a bare `yarn` exits 127 on a runner carrying nothing but docker and
|
|
||||||
git, or runs under another node.
|
|
||||||
- [ ] No `docker build` in `script/` leaves a dangling image behind:
|
- [ ] No `docker build` in `script/` leaves a dangling image behind:
|
||||||
`script/lint` and `script/test` write no image, and `script/docker` and
|
`script/lint` and `script/test` write no image, and `script/docker` and
|
||||||
`script/cibuild` tag theirs
|
`script/cibuild` tag theirs
|
||||||
@@ -241,11 +252,12 @@ are thin shims calling them. Model scripts:
|
|||||||
|
|
||||||
- [ ] `make check` passes
|
- [ ] `make check` passes
|
||||||
- [ ] `make docker` succeeds
|
- [ ] `make docker` succeeds
|
||||||
- [ ] `script/cibuild` succeeds in a fresh clone on a host carrying what CI has
|
- [ ] `script/cibuild` succeeds in a fresh clone on a host carrying what CI has.
|
||||||
(the runner image `docker.gitea.com/runner-images:ubuntu-latest`: docker,
|
That host is the runner image
|
||||||
git and node 24, with no yarn on `PATH`), and demonstrably executed the
|
`docker.gitea.com/runner-images:ubuntu-latest`, which has docker, git and
|
||||||
checks — a sub-second build, or `CACHED` on a step that lints or tests,
|
node 24. Take any yarn off `PATH` first. The run must show that the checks
|
||||||
one of the `RUN` steps below `ARG CACHEBUST`, means nothing ran. Above
|
executed. A sub-second build means nothing ran. So does `CACHED` on a step
|
||||||
|
that lints or tests, one of the `RUN` steps below `ARG CACHEBUST`. Above
|
||||||
that line, these steps are expected to show `CACHED`:
|
that line, these steps are expected to show `CACHED`:
|
||||||
- the steps that install system packages
|
- the steps that install system packages
|
||||||
- the steps that download Go's modules
|
- the steps that download Go's modules
|
||||||
@@ -259,8 +271,12 @@ are thin shims calling them. Model scripts:
|
|||||||
- [ ] No mutable image/package references. The one exception is a package from
|
- [ ] No mutable image/package references. The one exception is a package from
|
||||||
the distribution's own package index, which needs no version and no hash.
|
the distribution's own package index, which needs no version and no hash.
|
||||||
Pulling that index is also allowed. In a `Dockerfile`, the distribution is
|
Pulling that index is also allowed. In a `Dockerfile`, the distribution is
|
||||||
the pinned base image's. On a host, it is the host's. A package source
|
the pinned base image's. On a host, it is the host's, and the host's
|
||||||
|
package manager stands for it, nix and brew included. A package source
|
||||||
that a `Dockerfile` adds itself does not get this exception
|
that a `Dockerfile` adds itself does not get this exception
|
||||||
|
- [ ] node, yarn, npm, python, pip and venv come from the distribution wherever
|
||||||
|
possible, never from nvm, pyenv, corepack or `npm install -g yarn`. If the
|
||||||
|
distribution's version is too old, use a newer distribution
|
||||||
- [ ] No unnecessary files in repo root
|
- [ ] No unnecessary files in repo root
|
||||||
- [ ] All dates written as YYYY-MM-DD
|
- [ ] All dates written as YYYY-MM-DD
|
||||||
|
|
||||||
|
|||||||
+119
-60
@@ -25,6 +25,7 @@ style conventions are in separate documents:
|
|||||||
a remote source, including:
|
a remote source, including:
|
||||||
- Docker base images
|
- Docker base images
|
||||||
- Go modules
|
- Go modules
|
||||||
|
- Python packages
|
||||||
- npm packages
|
- npm packages
|
||||||
- GitHub Actions
|
- GitHub Actions
|
||||||
|
|
||||||
@@ -33,29 +34,45 @@ style conventions are in separate documents:
|
|||||||
dependency by its content hash:
|
dependency by its content hash:
|
||||||
- Docker: `@sha256:...`
|
- Docker: `@sha256:...`
|
||||||
- Go: the module hash in `go.sum`
|
- Go: the module hash in `go.sum`
|
||||||
|
- Python: the package hash in `Pipfile.lock`
|
||||||
- npm: the integrity hash in the lockfile
|
- npm: the integrity hash in the lockfile
|
||||||
- GitHub Actions: `@<commit-sha>`
|
- GitHub Actions: `@<commit-sha>`
|
||||||
|
|
||||||
This also means never `curl | bash` to install tools like pyenv, nvm,
|
This also means never `curl | bash` to install a tool such as rustup.
|
||||||
rustup, etc. Instead, download a specific release archive from GitHub,
|
Instead, download a specific release archive from GitHub, verify its hash,
|
||||||
verify its hash, and only then install. The hash is hardcoded in the
|
and only then install. The hash is hardcoded in the Dockerfile or script.
|
||||||
Dockerfile or script. Unverified install scripts are arbitrary remote code
|
Unverified install scripts are arbitrary remote code execution. Double-check
|
||||||
execution. Double-check every external reference in every file before
|
every external reference in every file before committing.
|
||||||
committing.
|
|
||||||
|
|
||||||
There is one exception: a package from the distribution's own package index
|
There is one exception: a package from the distribution's own package index
|
||||||
needs no version and no hash. Pulling that index is also allowed. The
|
needs no version and no hash. Pulling that index is also allowed. The
|
||||||
distribution is trusted. In a `Dockerfile`, it is the pinned base image's
|
distribution is trusted. In a `Dockerfile`, it is the pinned base image's
|
||||||
distribution. On a host, it is the host's. These commands are examples:
|
distribution. On a host, it is the host's. The host's package manager stands
|
||||||
|
for its distribution, and that includes nix and brew. These commands are
|
||||||
|
examples:
|
||||||
- `apt-get update`
|
- `apt-get update`
|
||||||
- `apt-get install`
|
- `apt-get install`
|
||||||
- `apk update`
|
- `apk update`
|
||||||
- `apk add`
|
- `apk add`
|
||||||
|
|
||||||
|
These come from the distribution wherever possible:
|
||||||
|
- node
|
||||||
|
- yarn
|
||||||
|
- npm
|
||||||
|
- python
|
||||||
|
- pip and venv, the installer tools bundled with python
|
||||||
|
|
||||||
|
If the distribution's version is too old, use a newer distribution, never a
|
||||||
|
download. That rules out all of these:
|
||||||
|
- nvm
|
||||||
|
- pyenv
|
||||||
|
- corepack
|
||||||
|
- `npm install -g yarn`
|
||||||
|
|
||||||
These are not trusted, so everything from them stays pinned by hash:
|
These are not trusted, so everything from them stays pinned by hash:
|
||||||
- Docker Hub
|
- Docker Hub
|
||||||
- PyPI
|
- PyPI
|
||||||
- npm
|
- the npm registry
|
||||||
- `go get`
|
- `go get`
|
||||||
|
|
||||||
A package source that a `Dockerfile` adds itself is not the distribution's
|
A package source that a `Dockerfile` adds itself is not the distribution's
|
||||||
@@ -99,18 +116,33 @@ style conventions are in separate documents:
|
|||||||
initialization.
|
initialization.
|
||||||
|
|
||||||
`script/bootstrap` installs all dependencies idempotently and assumes
|
`script/bootstrap` installs all dependencies idempotently and assumes
|
||||||
nothing is present. Base tools come from nix, apt, brew, or apk, detected in
|
nothing is present. Base tools, node and yarn come from nix, apt, brew, or
|
||||||
that order; apt runs noninteractive. For node it uses the installed node
|
apk, detected in that order; apt runs noninteractive. Each one is installed
|
||||||
only when its major version is the pinned one. When node is missing or has
|
only when it is missing. A tool from outside the distribution is never
|
||||||
another major, it installs the PINNED node version via nvm, and installs
|
installed as "latest" or "lts"; it is always an exact version.
|
||||||
yarn and the packages under it. The CI runner image ships node 24, for
|
|
||||||
example. nvm itself is installed first if missing, from a hash-verified
|
`script/bootstrap` installs node, yarn and the node packages only when
|
||||||
GitHub release archive, never `curl | sh`. bash is installed as an explicit
|
`yarn.lock` is in the tree. With no `yarn.lock`, it installs no node and no
|
||||||
prerequisite, since nvm requires bash. Only the major version is compared
|
yarn, and it does not check yarn's version.
|
||||||
because the `Dockerfile` stages start from a node image whose exact version
|
|
||||||
is not the pin, and nvm cannot install a prebuilt node on alpine. yarn is
|
Debian and Ubuntu name the yarn command `yarnpkg`. `script/bootstrap`,
|
||||||
pinned via `corepack prepare yarn@<version> --activate`. Never install
|
`script/fmt` and `script/fmt-check` each carry the same small function that
|
||||||
"latest" or "lts"; always exact versions.
|
runs whichever name is present. With neither name on `PATH`, the function
|
||||||
|
prints one line saying to run `script/bootstrap`, and exits 1.
|
||||||
|
|
||||||
|
With `yarn.lock` in the tree, `script/bootstrap` stops with a one-line
|
||||||
|
message when yarn's major version is not 1, before it installs any node
|
||||||
|
packages. Debian 13 and Ubuntu 26.04 package yarn 4. With
|
||||||
|
`--frozen-lockfile`, yarn 4 refuses a yarn 1 `yarn.lock`. Without that flag,
|
||||||
|
it rewrites the file in its own format, with its own hashes in place of
|
||||||
|
these. A host whose distribution packages yarn 4 moves to one that packages
|
||||||
|
yarn 1, such as Alpine or Ubuntu 24.04.
|
||||||
|
|
||||||
|
A `Dockerfile` stage copies `yarn.lock` before it runs `script/bootstrap`
|
||||||
|
only if that stage needs yarn. A stage that needs yarn starts from an image
|
||||||
|
whose distribution packages yarn 1, such as Alpine. The Python and Go stages
|
||||||
|
below need no yarn, so they copy no `yarn.lock` before that step and never
|
||||||
|
reach the stop.
|
||||||
|
|
||||||
`script/cibuild` runs the CI build, and the Gitea workflow calls it. The
|
`script/cibuild` runs the CI build, and the Gitea workflow calls it. The
|
||||||
script changes to the repo root, runs `script/bootstrap`, runs
|
script changes to the repo root, runs `script/bootstrap`, runs
|
||||||
@@ -118,15 +150,7 @@ style conventions are in separate documents:
|
|||||||
runs `script/bootstrap` first**, because the workflow checks out the repo
|
runs `script/bootstrap` first**, because the workflow checks out the repo
|
||||||
and runs nothing else, while `script/fmt-check` runs the formatter on the
|
and runs nothing else, while `script/fmt-check` runs the formatter on the
|
||||||
host. On a pristine checkout with nothing installed, the run dies there,
|
host. On a pristine checkout with nothing installed, the run dies there,
|
||||||
after the containerised gates have passed. **The bootstrap alone is not
|
after the containerised gates have passed.
|
||||||
enough.** When `script/bootstrap` installs node and yarn under nvm, it
|
|
||||||
leaves neither on the `PATH` of the shell that called it. A bare `yarn` then
|
|
||||||
either exits 127 or runs under another node. The host entrypoints that need
|
|
||||||
yarn, `script/fmt` and `script/fmt-check`, therefore use the same test as
|
|
||||||
`script/bootstrap`. When the node on `PATH` has the pinned major version,
|
|
||||||
they run the `yarn` on `PATH`. Otherwise they load nvm through
|
|
||||||
`$HOME/.nvm/nvm.sh` and run yarn under the pinned node. A runner carrying
|
|
||||||
nothing but docker and git then gets through `script/check`.
|
|
||||||
|
|
||||||
Four further scripts are our own extensions to the standard:
|
Four further scripts are our own extensions to the standard:
|
||||||
- `script/check` runs `script/test`, `script/lint` and `script/fmt-check`.
|
- `script/check` runs `script/test`, `script/lint` and `script/fmt-check`.
|
||||||
@@ -175,10 +199,11 @@ style conventions are in separate documents:
|
|||||||
does for its yarn packages
|
does for its yarn packages
|
||||||
|
|
||||||
The development environment stage installs development prerequisites by
|
The development environment stage installs development prerequisites by
|
||||||
running `script/bootstrap` rather than duplicating its installs inline. A
|
running `script/bootstrap`. Only system packages may be installed inline
|
||||||
stage that runs `script/bootstrap` COPYs `script/` and the dependency
|
above it, so that check builds keep them; the `prompts` repo's own
|
||||||
manifests (`package.json` + `yarn.lock`, `go.mod` + `go.sum`, etc.) before
|
`Dockerfile` does this. A stage that runs `script/bootstrap` COPYs `script/`
|
||||||
running it.
|
and the dependency manifests it needs (`package.json` + `yarn.lock`,
|
||||||
|
`go.mod` + `go.sum`, etc.) before running it.
|
||||||
|
|
||||||
- **Linting and testing run in Docker, as phases of the `Dockerfile`.** There is
|
- **Linting and testing run in Docker, as phases of the `Dockerfile`.** There is
|
||||||
no separate lint file. `script/lint` and `script/test` each build one phase
|
no separate lint file. `script/lint` and `script/test` each build one phase
|
||||||
@@ -491,10 +516,18 @@ style conventions are in separate documents:
|
|||||||
- pipenv and its dependencies
|
- pipenv and its dependencies
|
||||||
- the packages from `Pipfile.lock`
|
- the packages from `Pipfile.lock`
|
||||||
|
|
||||||
`script/bootstrap` installs no Python there: the tests use the base
|
`script/bootstrap` installs no Python there: the tests use the
|
||||||
image's Python, which the image's hash pins. If it installs anything
|
distribution's Python, which a step above it installs. If it installs
|
||||||
else there, the step goes after `ARG CACHEBUST`.
|
anything else there, the step goes after `ARG CACHEBUST`.
|
||||||
|
|
||||||
|
- Debian's Python refuses a system-wide `pip install`, so pipenv goes in a
|
||||||
|
virtual environment at `$HOME/.pipenv`. `script/bootstrap` creates it with
|
||||||
|
`python3 -m venv` when it is missing, then uses that environment's `pip`
|
||||||
|
and `pipenv`. On a host, it first installs the distribution's python and
|
||||||
|
venv packages when they are missing. One script then works on a host and
|
||||||
|
in a `Dockerfile`. `Pipfile` names no `python_version`, because
|
||||||
|
`pipenv install --deploy` stops when the distribution's Python differs
|
||||||
|
from it.
|
||||||
- PyPI is not trusted, so `script/bootstrap` installs pipenv and its
|
- PyPI is not trusted, so `script/bootstrap` installs pipenv and its
|
||||||
dependencies by hash. It does so with `pip install --require-hashes -r`
|
dependencies by hash. It does so with `pip install --require-hashes -r`
|
||||||
and a requirements file in `script/` that lists each of them with `==` and
|
and a requirements file in `script/` that lists each of them with `==` and
|
||||||
@@ -504,37 +537,65 @@ style conventions are in separate documents:
|
|||||||
- installs only what `Pipfile.lock` names
|
- installs only what `Pipfile.lock` names
|
||||||
- checks every package against the lockfile's hashes
|
- checks every package against the lockfile's hashes
|
||||||
- fails when the lockfile is out of date with `Pipfile`
|
- fails when the lockfile is out of date with `Pipfile`
|
||||||
|
- `script/bootstrap` sets `PIP_ONLY_BINARY=:all:` for both installs, and
|
||||||
|
pipenv passes it on to pip. A package with no ready-built wheel for the
|
||||||
|
image then stops the install. Without the setting, pip builds that package
|
||||||
|
from source and downloads the build tools without a hash check.
|
||||||
- A kept layer holds nothing a new install would refuse: pip checked the
|
- A kept layer holds nothing a new install would refuse: pip checked the
|
||||||
hashes of pipenv and its dependencies, and pipenv checked the hashes of
|
hashes of pipenv and its dependencies, and pipenv checked the hashes of
|
||||||
the lockfile's packages. pipenv creates its virtual environment with the
|
the lockfile's packages. pipenv creates its virtual environment with the
|
||||||
`virtualenv` package. The packages that `virtualenv` adds come from wheels
|
`virtualenv` package. The packages that `virtualenv` adds come from wheels
|
||||||
inside that package, so the hash pip checked for it covers them. Two
|
inside that package, so the hash pip checked for it covers them. System
|
||||||
things in the layer are not fixed by a hash:
|
packages, Python among them, are not fixed by a hash. They keep the
|
||||||
- A package with no ready-built wheel for the image is built from
|
versions the package manager gave when the layer was built; a new install
|
||||||
source. pip downloads the build tools without a hash check, so a kept
|
may get newer ones.
|
||||||
layer's build of such a package may differ from a new one.
|
|
||||||
- System packages keep the versions the package manager gave when the
|
|
||||||
layer was built; a new install may get newer ones.
|
|
||||||
- As with Go's modules, the packages are kept in a layer, not a cache mount.
|
- As with Go's modules, the packages are kept in a layer, not a cache mount.
|
||||||
A cache mount is shared by every build on the host, and nothing checks its
|
A cache mount is shared by every build on the host, and nothing checks its
|
||||||
files again.
|
files again.
|
||||||
|
|
||||||
In the example below, `script/bootstrap` does all of these:
|
A Python repository's `script/bootstrap` adds these lines in `main`, above
|
||||||
- finds `make` and `git` already installed
|
the last `echo`. The `ensurepip` test fails when python is missing, and also
|
||||||
- installs pipenv and its dependencies by hash
|
when Debian's venv package is missing:
|
||||||
- runs `pipenv install --dev --deploy`
|
|
||||||
|
```sh
|
||||||
|
if [ ! -x "$HOME/.pipenv/bin/pip" ]; then
|
||||||
|
if ! python3 -m ensurepip --version >/dev/null 2>&1; then
|
||||||
|
pkg_install python3 python3-venv python3 python3
|
||||||
|
fi
|
||||||
|
python3 -m venv "$HOME/.pipenv"
|
||||||
|
fi
|
||||||
|
export PIP_ONLY_BINARY=:all:
|
||||||
|
"$HOME/.pipenv/bin/pip" install --require-hashes -r script/requirements.txt
|
||||||
|
"$HOME/.pipenv/bin/pipenv" install --dev --deploy
|
||||||
|
```
|
||||||
|
|
||||||
|
Make `script/requirements.txt` once with
|
||||||
|
`echo pipenv | pip-compile --generate-hashes --allow-unsafe -o script/requirements.txt -`,
|
||||||
|
from Alpine's `py3-pip-tools` package, because Debian and Ubuntu do not
|
||||||
|
package it. Make the first `Pipfile.lock` with
|
||||||
|
`$HOME/.pipenv/bin/pipenv lock`, after the `pip install` line has installed
|
||||||
|
pipenv.
|
||||||
|
|
||||||
|
In the example below, `script/bootstrap` finds `make`, `git`, python and
|
||||||
|
venv already installed. The example copies no `yarn.lock`, so
|
||||||
|
`script/bootstrap` installs no node and no yarn.
|
||||||
|
|
||||||
```dockerfile
|
```dockerfile
|
||||||
# Test phase. Both pytest runs share one 90-second timeout, so a hang
|
# Test phase. Both pytest runs share one 90-second timeout, so a hang
|
||||||
# uses it once and gets no rerun.
|
# uses it once and gets no rerun.
|
||||||
# python:3.x-slim, YYYY-MM-DD
|
# debian:trixie-slim, YYYY-MM-DD
|
||||||
FROM python@sha256:... AS test
|
FROM debian@sha256:... AS test
|
||||||
# make and git are base tools that script/bootstrap installs when they
|
# Python, pip and venv come from the distribution, which is trusted, so
|
||||||
# are missing. Installed here, they stay in their own layer when
|
# they need no version and no hash. make and git are base tools that
|
||||||
# script/ changes.
|
# script/bootstrap installs when they are missing. Installed here, they
|
||||||
|
# stay in their own layer when script/ changes.
|
||||||
RUN apt-get update \
|
RUN apt-get update \
|
||||||
&& apt-get install -y --no-install-recommends make git \
|
&& apt-get install -y --no-install-recommends ca-certificates make git \
|
||||||
|
python3 python3-venv \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
# script/bootstrap installs pipenv in a virtual environment at
|
||||||
|
# $HOME/.pipenv, and the tests below run that pipenv.
|
||||||
|
ENV PATH=/root/.pipenv/bin:$PATH
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
COPY script/ script/
|
COPY script/ script/
|
||||||
COPY Pipfile Pipfile.lock ./
|
COPY Pipfile Pipfile.lock ./
|
||||||
@@ -669,16 +730,16 @@ style conventions are in separate documents:
|
|||||||
|
|
||||||
```makefile
|
```makefile
|
||||||
test:
|
test:
|
||||||
@timeout 90 sh -c 'python -m pytest || { \
|
@timeout 90 sh -c 'pipenv run pytest || { \
|
||||||
echo "--- Rerunning with -v for details ---"; \
|
echo "--- Rerunning with -v for details ---"; \
|
||||||
python -m pytest -v; exit 1; }'
|
pipenv run pytest -v; exit 1; }'
|
||||||
```
|
```
|
||||||
|
|
||||||
Both pytest runs share that one 90-second timeout, as in the `Dockerfile`
|
Both pytest runs share that one 90-second timeout, as in the `Dockerfile`
|
||||||
example above, so a hang uses it once and gets no rerun. Ctrl-C does not
|
example above, so a hang uses it once and gets no rerun. Ctrl-C does not
|
||||||
stop a running `make test`, which goes on until the tests end or the 90
|
stop a running `make test`, which goes on until the tests end or the 90
|
||||||
seconds run out. On a Mac without `timeout`, the target fails until it is
|
seconds run out. On a Mac without `timeout`, the target fails until it is
|
||||||
installed.
|
installed. On a host, the target needs `$HOME/.pipenv/bin` on `PATH`.
|
||||||
|
|
||||||
The `exit 1` ensures the target always fails after a rerun. The first run
|
The `exit 1` ensures the target always fails after a rerun. The first run
|
||||||
already proved the tests are broken, so the build must not pass even if a
|
already proved the tests are broken, so the build must not pass even if a
|
||||||
@@ -857,10 +918,8 @@ style conventions are in separate documents:
|
|||||||
function defined and never invoked has the same exit status and the same
|
function defined and never invoked has the same exit status and the same
|
||||||
empty output as one that worked.
|
empty output as one that worked.
|
||||||
|
|
||||||
Node is the exception to the whole-token comparison. The canonical
|
A tool from the distribution has no pin, so `script/bootstrap` installs it
|
||||||
`script/bootstrap` compares only node's major version. That is because the
|
when it is missing. make, git, node and yarn are such tools.
|
||||||
`Dockerfile` stages start from a node 22 alpine image, which is not the
|
|
||||||
exact pin, and nvm cannot install a prebuilt node on alpine.
|
|
||||||
|
|
||||||
Keep it POSIX sh: no arrays, no `[[`, no `grep -P`.
|
Keep it POSIX sh: no arrays, no `[[`, no `grep -P`.
|
||||||
|
|
||||||
|
|||||||
+30
-96
@@ -1,24 +1,15 @@
|
|||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
# script/bootstrap: install all dependencies needed to build and develop
|
# script/bootstrap: install all dependencies needed to build and develop
|
||||||
# this repo. Idempotent: every install is guarded by a check so already
|
# this repo. Idempotent: every install is guarded by a check so already
|
||||||
# installed tools are skipped. Base tooling comes from nix, apt, brew,
|
# installed tools are skipped. Base tools, node and yarn come from nix,
|
||||||
# or apk (detected in that order); assumes nothing is present. The
|
# apt, brew, or apk (detected in that order), with no version and no
|
||||||
# installed node is used only when its major version is the pinned one;
|
# hash, because the distribution is trusted. It installs node, yarn and
|
||||||
# otherwise (node missing, or another major) the pinned version is
|
# the node packages only when yarn.lock is in the tree. Assumes nothing
|
||||||
# installed via nvm (installing nvm itself first, from a hash-verified
|
# is present.
|
||||||
# release archive, never curl | sh), and yarn and the packages are
|
|
||||||
# installed under it.
|
|
||||||
set -eu
|
set -eu
|
||||||
|
|
||||||
ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
|
ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
|
||||||
|
|
||||||
# Pinned versions, 2026-07-06
|
|
||||||
NODE_VERSION="22.17.0"
|
|
||||||
NVM_VERSION="0.40.3"
|
|
||||||
# sha256 of https://github.com/nvm-sh/nvm/archive/refs/tags/v0.40.3.tar.gz
|
|
||||||
NVM_SHA256="5f4d6aaa04a177dc93c985e31dbc411ab6b8c6e1e21d8015dbc1372625fcd1d0"
|
|
||||||
YARN_VERSION="1.22.22"
|
|
||||||
|
|
||||||
PKGMGR=""
|
PKGMGR=""
|
||||||
SUDO=""
|
SUDO=""
|
||||||
APT_UPDATED=""
|
APT_UPDATED=""
|
||||||
@@ -67,99 +58,42 @@ missing() {
|
|||||||
! command -v "$1" >/dev/null 2>&1
|
! command -v "$1" >/dev/null 2>&1
|
||||||
}
|
}
|
||||||
|
|
||||||
# verify_sha256 <file> <expected-hash>
|
# Debian and Ubuntu name the yarn command yarnpkg.
|
||||||
verify_sha256() {
|
run_yarn() {
|
||||||
if command -v sha256sum >/dev/null 2>&1; then
|
if command -v yarn >/dev/null 2>&1; then
|
||||||
actual="$(sha256sum "$1" | cut -d' ' -f1)"
|
yarn "$@"
|
||||||
|
elif command -v yarnpkg >/dev/null 2>&1; then
|
||||||
|
yarnpkg "$@"
|
||||||
else
|
else
|
||||||
actual="$(shasum -a 256 "$1" | cut -d' ' -f1)"
|
echo "no yarn; run script/bootstrap first" >&2
|
||||||
fi
|
|
||||||
if [ "$actual" != "$2" ]; then
|
|
||||||
echo "bootstrap: sha256 mismatch for $1" >&2
|
|
||||||
echo " expected: $2" >&2
|
|
||||||
echo " actual: $actual" >&2
|
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
# nvm is a bash script; run a command in a bash with nvm loaded
|
|
||||||
nvm_sh() {
|
|
||||||
bash -c ". \"\$HOME/.nvm/nvm.sh\" && $*"
|
|
||||||
}
|
|
||||||
|
|
||||||
ensure_nvm() {
|
|
||||||
[ -s "$HOME/.nvm/nvm.sh" ] && return 0
|
|
||||||
# nvm prerequisites; nvm itself requires bash
|
|
||||||
if missing bash; then pkg_install bash bash bash bash; fi
|
|
||||||
if missing curl; then pkg_install curl curl curl curl; fi
|
|
||||||
if missing git; then pkg_install git git git git; fi
|
|
||||||
tmp="$(mktemp -d)"
|
|
||||||
curl -fsSL -o "$tmp/nvm.tar.gz" \
|
|
||||||
"https://github.com/nvm-sh/nvm/archive/refs/tags/v${NVM_VERSION}.tar.gz"
|
|
||||||
verify_sha256 "$tmp/nvm.tar.gz" "$NVM_SHA256"
|
|
||||||
mkdir -p "$HOME/.nvm"
|
|
||||||
tar -xzf "$tmp/nvm.tar.gz" -C "$HOME/.nvm" --strip-components=1
|
|
||||||
rm -rf "$tmp"
|
|
||||||
}
|
|
||||||
|
|
||||||
# True when the node on PATH has the pinned major version. Only the
|
|
||||||
# major is compared: the Dockerfile stages start from a node 22 alpine
|
|
||||||
# image whose exact version is not the pin, and nvm cannot install a
|
|
||||||
# prebuilt node on alpine. Another major is not used: the pinned yarn 1
|
|
||||||
# prints a deprecation warning under node 24. script/fmt and
|
|
||||||
# script/fmt-check carry this function unchanged, to run the yarn
|
|
||||||
# installed here.
|
|
||||||
node_is_pinned_major() {
|
|
||||||
if ! command -v node >/dev/null 2>&1; then return 1; fi
|
|
||||||
installed="$(node --version)"
|
|
||||||
installed="${installed#v}"
|
|
||||||
[ "${installed%%.*}" = "${NODE_VERSION%%.*}" ]
|
|
||||||
}
|
|
||||||
|
|
||||||
ensure_node() {
|
|
||||||
if node_is_pinned_major; then
|
|
||||||
echo "bootstrap: using node $(node --version)"
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
ensure_nvm
|
|
||||||
nvm_sh "nvm install $NODE_VERSION"
|
|
||||||
nvm_sh "nvm use $NODE_VERSION >/dev/null && \
|
|
||||||
echo \"bootstrap: using node \$(node --version)\""
|
|
||||||
}
|
|
||||||
|
|
||||||
ensure_yarn() {
|
|
||||||
if ! node_is_pinned_major; then
|
|
||||||
nvm_sh "nvm use $NODE_VERSION >/dev/null && corepack enable && \
|
|
||||||
corepack prepare yarn@$YARN_VERSION --activate"
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
if ! missing yarn; then return 0; fi
|
|
||||||
if ! missing corepack; then
|
|
||||||
corepack enable
|
|
||||||
corepack prepare "yarn@$YARN_VERSION" --activate
|
|
||||||
else
|
|
||||||
npm install -g "yarn@$YARN_VERSION"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
install_js_deps() {
|
|
||||||
if node_is_pinned_major; then
|
|
||||||
yarn install --frozen-lockfile
|
|
||||||
else
|
|
||||||
nvm_sh "nvm use $NODE_VERSION >/dev/null && cd \"$ROOT\" && \
|
|
||||||
yarn install --frozen-lockfile"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
main() {
|
main() {
|
||||||
cd "$ROOT"
|
cd "$ROOT"
|
||||||
|
|
||||||
if missing make; then pkg_install gnumake make make make; fi
|
if missing make; then pkg_install gnumake make make make; fi
|
||||||
if missing git; then pkg_install git git git git; fi
|
if missing git; then pkg_install git git git git; fi
|
||||||
|
|
||||||
ensure_node
|
# A Dockerfile stage that needs no yarn copies no yarn.lock.
|
||||||
ensure_yarn
|
if [ -f yarn.lock ]; then
|
||||||
install_js_deps
|
if missing node; then pkg_install nodejs nodejs node nodejs; fi
|
||||||
|
if missing yarn && missing yarnpkg; then
|
||||||
|
pkg_install yarn yarnpkg yarn yarn
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Debian 13 and Ubuntu 26.04 package yarn 4, which refuses a yarn 1
|
||||||
|
# yarn.lock, or rewrites it in its own format.
|
||||||
|
yarn_version="$(run_yarn --version)"
|
||||||
|
if [ "${yarn_version%%.*}" != "1" ]; then
|
||||||
|
echo "bootstrap: yarn $yarn_version refuses this yarn 1 yarn.lock;" \
|
||||||
|
"use a distribution that packages yarn 1" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
run_yarn install --frozen-lockfile
|
||||||
|
fi
|
||||||
|
|
||||||
echo "bootstrap complete"
|
echo "bootstrap complete"
|
||||||
}
|
}
|
||||||
|
|||||||
+7
-24
@@ -4,33 +4,16 @@ set -eu
|
|||||||
|
|
||||||
ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
|
ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
|
||||||
|
|
||||||
# Must match the pin in script/bootstrap.
|
# Debian and Ubuntu name the yarn command yarnpkg.
|
||||||
NODE_VERSION="22.17.0"
|
|
||||||
|
|
||||||
# True when the node on PATH has the pinned major version: the same
|
|
||||||
# function as in script/bootstrap, which uses it to decide where it
|
|
||||||
# installs yarn.
|
|
||||||
node_is_pinned_major() {
|
|
||||||
if ! command -v node >/dev/null 2>&1; then return 1; fi
|
|
||||||
installed="$(node --version)"
|
|
||||||
installed="${installed#v}"
|
|
||||||
[ "${installed%%.*}" = "${NODE_VERSION%%.*}" ]
|
|
||||||
}
|
|
||||||
|
|
||||||
# Run the yarn script/bootstrap installed: the yarn on PATH when the
|
|
||||||
# node on PATH has the pinned major version, and otherwise yarn under
|
|
||||||
# the pinned node in nvm, which bootstrap leaves off the PATH of the
|
|
||||||
# shell that called it. nvm is a bash script, hence the subshell.
|
|
||||||
run_yarn() {
|
run_yarn() {
|
||||||
if node_is_pinned_major && command -v yarn >/dev/null 2>&1; then
|
if command -v yarn >/dev/null 2>&1; then
|
||||||
exec yarn "$@"
|
yarn "$@"
|
||||||
fi
|
elif command -v yarnpkg >/dev/null 2>&1; then
|
||||||
if node_is_pinned_major || [ ! -s "$HOME/.nvm/nvm.sh" ]; then
|
yarnpkg "$@"
|
||||||
echo "fmt: no yarn; run script/bootstrap first" >&2
|
else
|
||||||
|
echo "no yarn; run script/bootstrap first" >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
exec bash -c '. "$HOME/.nvm/nvm.sh" && nvm use "$1" >/dev/null &&
|
|
||||||
shift && exec yarn "$@"' bash "$NODE_VERSION" "$@"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
main() {
|
main() {
|
||||||
|
|||||||
+7
-24
@@ -4,33 +4,16 @@ set -eu
|
|||||||
|
|
||||||
ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
|
ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
|
||||||
|
|
||||||
# Must match the pin in script/bootstrap.
|
# Debian and Ubuntu name the yarn command yarnpkg.
|
||||||
NODE_VERSION="22.17.0"
|
|
||||||
|
|
||||||
# True when the node on PATH has the pinned major version: the same
|
|
||||||
# function as in script/bootstrap, which uses it to decide where it
|
|
||||||
# installs yarn.
|
|
||||||
node_is_pinned_major() {
|
|
||||||
if ! command -v node >/dev/null 2>&1; then return 1; fi
|
|
||||||
installed="$(node --version)"
|
|
||||||
installed="${installed#v}"
|
|
||||||
[ "${installed%%.*}" = "${NODE_VERSION%%.*}" ]
|
|
||||||
}
|
|
||||||
|
|
||||||
# Run the yarn script/bootstrap installed: the yarn on PATH when the
|
|
||||||
# node on PATH has the pinned major version, and otherwise yarn under
|
|
||||||
# the pinned node in nvm, which bootstrap leaves off the PATH of the
|
|
||||||
# shell that called it. nvm is a bash script, hence the subshell.
|
|
||||||
run_yarn() {
|
run_yarn() {
|
||||||
if node_is_pinned_major && command -v yarn >/dev/null 2>&1; then
|
if command -v yarn >/dev/null 2>&1; then
|
||||||
exec yarn "$@"
|
yarn "$@"
|
||||||
fi
|
elif command -v yarnpkg >/dev/null 2>&1; then
|
||||||
if node_is_pinned_major || [ ! -s "$HOME/.nvm/nvm.sh" ]; then
|
yarnpkg "$@"
|
||||||
echo "fmt-check: no yarn; run script/bootstrap first" >&2
|
else
|
||||||
|
echo "no yarn; run script/bootstrap first" >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
exec bash -c '. "$HOME/.nvm/nvm.sh" && nvm use "$1" >/dev/null &&
|
|
||||||
shift && exec yarn "$@"' bash "$NODE_VERSION" "$@"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
main() {
|
main() {
|
||||||
|
|||||||
Reference in New Issue
Block a user