Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 1 addition & 9 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,7 @@ jobs:
with:
go-version-file: ./go.mod

- name: Download embedded PHP files
- name: Download the embedded PHP
shell: bash
run: |
# Line endings may be converted on checkout.
Expand All @@ -185,9 +185,7 @@ jobs:
mkdir -p internal/legacy/archives
curl -fSL "https://github.com/upsun/cli-php-builds/releases/download/php-$php_version-$(var PHP_BUILD)/$asset" \
-o internal/legacy/archives/php_windows.exe
curl -fSL "https://curl.se/ca/cacert-$(var CACERT_DATE).pem" -o internal/legacy/archives/cacert.pem
echo "$php_sha256 internal/legacy/archives/php_windows.exe" | sha256sum -c -
echo "$(var CACERT_SHA256) internal/legacy/archives/cacert.pem" | sha256sum -c -
# Only needed so that the package builds.
touch internal/legacy/archives/platform.phar
touch internal/legacy/archives/commands-upsun.json internal/legacy/archives/commands-platformsh.json
Expand All @@ -197,9 +195,3 @@ jobs:
env:
CLI_TEST_MODIFY_CERT_STORE: '1'
run: go test -v -count=1 -timeout 3m -run TestWindows ./internal/legacy/

# Reading the certificate store happens before every legacy command.
- name: Measure building the CA bundle
if: always()
shell: bash
run: go test -run '^$' -bench BenchmarkWindows -benchmem ./internal/legacy/
2 changes: 1 addition & 1 deletion .github/workflows/update-php.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# Opens or updates a pull request when there is a new build of the embedded PHP
# (from upsun/cli-php-builds) or a new CA bundle. See scripts/update-php.sh.
# (from upsun/cli-php-builds). See scripts/update-php.sh.
name: Update PHP

on:
Expand Down
4 changes: 2 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ The CLI operates as a wrapper around a legacy PHP CLI:
- All platforms use static binaries built with [static-php-cli](https://github.com/crazywhalecc/static-php-cli)
- Supported platforms: linux/amd64, linux/arm64, darwin/amd64, darwin/arm64, windows/amd64
- Extensions included: curl, filter, openssl, pcntl (Unix), phar, posix (Unix), zlib
- Windows requires `cacert.pem` for OpenSSL (embedded separately), pinned by date and checksum (`CACERT_DATE`, `CACERT_SHA256`)
- On Windows, curl is built with Schannel, which verifies certificates with Windows (its certificate store, distrust list and revocation). No CA file is set, as Schannel would use it instead of the store, so the legacy CLI must make HTTPS requests with curl (Guzzle), not PHP streams (OpenSSL), and uses `CURLSSLOPT_REVOKE_BEST_EFFORT`, as roots installed for TLS inspection often have no revocation information

**Downloading PHP Binaries**:
```bash
Expand All @@ -147,7 +147,7 @@ make download-php
```

**Upgrading PHP Version**:
- The `update-php` workflow runs daily: it finds the latest cli-php-builds release for the current PHP minor version, and the latest CA bundle, and opens or updates a pull request that updates `PHP_VERSION`, `PHP_BUILD`, the checksums and the CA bundle (with `scripts/update-php.sh`). The pull request lists the changed source archives (from the releases' `sources.json`)
- The `update-php` workflow runs daily: it finds the latest cli-php-builds release for the current PHP minor version, and opens or updates a pull request that updates `PHP_VERSION`, `PHP_BUILD` and the checksums (with `scripts/update-php.sh`). The pull request lists the changed source archives (from the releases' `sources.json`)
- For a new PHP minor version: trigger the build workflow at [upsun/cli-php-builds](https://github.com/upsun/cli-php-builds/actions) with the new version, then set `PHP_VERSION` to it and `PHP_BUILD` to `0` (builds are numbered from 1), run `scripts/update-php.sh <title-file> <body-file>`, and `make download-php` to check the downloads

## Development Notes
Expand Down
20 changes: 5 additions & 15 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,11 +1,7 @@
# The PHP version, and the cli-php-builds build number (rebuilt when a library is updated).
# Checksums of the binaries are pinned in PHP_SHA256SUMS. Updated by the update-php workflow.
PHP_VERSION = 8.5.12
PHP_BUILD = 1

# The CA bundle embedded for PHP on Windows, from https://curl.se/docs/caextract.html.
CACERT_DATE = 2026-10-20
CACERT_SHA256 = 796f1389bfea13c0ed78f663b765baaff54a508e5483786bd6ece73c5e3e3cbe
PHP_BUILD = 2

.DEFAULT_GOAL := help

Expand Down Expand Up @@ -76,7 +72,7 @@ single: goreleaser $(ARCHIVES)/platform.phar $(LEGACY_INDEXES) php ## Build a si
PHP_VERSION=$(PHP_VERSION) goreleaser build --single-target --id=$(GORELEASER_ID) --snapshot --clean

.PHONY: snapshot
snapshot: goreleaser $(ARCHIVES)/platform.phar $(LEGACY_INDEXES) php $(ARCHIVES)/cacert.pem ## Build all release artifacts in dist/ without publishing (needs signing keys)
snapshot: goreleaser $(ARCHIVES)/platform.phar $(LEGACY_INDEXES) php ## Build all release artifacts in dist/ without publishing (needs signing keys)
ifndef RSA_SIGNING_KEY_FILE
$(error RSA_SIGNING_KEY_FILE is not set. Set it to the path of your RSA private key for APK signing, or use 'make snapshot-no-nfpm' to skip packaging.)
endif
Expand All @@ -101,7 +97,7 @@ php: $(ARCHIVES)/php_$(GOOS)_$(GOARCH) ## Download the PHP binary for the curren

# Download PHP binaries for all platforms.
.PHONY: download-php
download-php: $(PHP_UNIX_BINARIES) $(ARCHIVES)/php_windows.exe $(ARCHIVES)/cacert.pem
download-php: $(PHP_UNIX_BINARIES) $(ARCHIVES)/php_windows.exe

# Build the legacy CLI phar from the legacy/ subdirectory.
# The self:build command handles composer --no-dev rebuild, Box installation, and cache warming.
Expand Down Expand Up @@ -147,13 +143,7 @@ $(ARCHIVES)/php_windows.exe: $(ARCHIVES)/php-version
$(call verify-sha256,$@,$(call php-sha256,php-$(PHP_VERSION)-windows-amd64.exe))

.PHONY: $(ARCHIVES)/php_windows_amd64
$(ARCHIVES)/php_windows_amd64: $(ARCHIVES)/php_windows.exe $(ARCHIVES)/cacert.pem

.PHONY: $(ARCHIVES)/cacert.pem
$(ARCHIVES)/cacert.pem:
mkdir -p $(ARCHIVES)
curl -fSL https://curl.se/ca/cacert-$(CACERT_DATE).pem -o $@
$(call verify-sha256,$@,$(CACERT_SHA256))
$(ARCHIVES)/php_windows_amd64: $(ARCHIVES)/php_windows.exe

.PHONY: FORCE
FORCE:
Expand All @@ -162,7 +152,7 @@ FORCE:

# Create and publish a release.
.PHONY: release
release: goreleaser clean $(ARCHIVES)/platform.phar $(LEGACY_INDEXES) php $(ARCHIVES)/cacert.pem
release: goreleaser clean $(ARCHIVES)/platform.phar $(LEGACY_INDEXES) php
ifndef RSA_SIGNING_KEY_FILE
$(error RSA_SIGNING_KEY_FILE is not set. Set it to the path of your RSA private key for APK signing.)
endif
Expand Down
32 changes: 0 additions & 32 deletions internal/file/file.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,17 +18,6 @@ func WriteIfNeeded(destFilename string, source []byte, perm os.FileMode) error {
return Write(destFilename, source, perm)
}

// WriteIfChanged writes data to a destination file unless its complete
// contents already match. Unlike WriteIfNeeded, it is suitable for dynamic
// data which can change without changing its size or final bytes.
func WriteIfChanged(destFilename string, source []byte, perm os.FileMode) error {
matches, err := fullyMatches(destFilename, source)
if err != nil || matches {
return err
}
return Write(destFilename, source, perm)
}

// Write creates or overwrites a file, somewhat atomically, using a temporary file next to it.
func Write(path string, content []byte, fileMode fs.FileMode) error {
tmpFile := path + ".tmp"
Expand All @@ -39,27 +28,6 @@ func Write(path string, content []byte, fileMode fs.FileMode) error {
return os.Rename(tmpFile, path)
}

// fullyMatches checks if a file exists and its whole contents equal data. The
// size is compared first, so a file which cannot match is not read.
func fullyMatches(filename string, data []byte) (bool, error) {
fi, err := os.Stat(filename)
if err != nil {
if errors.Is(err, fs.ErrNotExist) {
return false, nil
}
return false, err
}
if fi.Size() != int64(len(data)) {
return false, nil
}

existing, err := os.ReadFile(filename)
if err != nil {
return false, err
}
return bytes.Equal(existing, data), nil
}

// probablyMatches checks if a file exists and matches the end of source data (up to checkSize bytes).
func probablyMatches(filename string, data []byte, checkSize int) (bool, error) {
f, err := os.Open(filename)
Expand Down
49 changes: 0 additions & 49 deletions internal/file/file_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,52 +79,3 @@ func TestWriteIfNeeded(t *testing.T) {
})
}
}

func TestWriteIfChanged(t *testing.T) {
source := []byte("new contents")

cases := []struct {
name string
initialData []byte
}{
{"a missing file", nil},
{"a shorter file", []byte("old")},
{"a longer file", []byte("old contents, but longer than the new ones")},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
destFile := filepath.Join(t.TempDir(), "dynamic-file")
if c.initialData != nil {
require.NoError(t, os.WriteFile(destFile, c.initialData, 0o600))
}

require.NoError(t, WriteIfChanged(destFile, source, 0o600))
actual, err := os.ReadFile(destFile)
require.NoError(t, err)
assert.Equal(t, source, actual)
})
}
}

func TestWriteIfChangedChecksTheWholeFile(t *testing.T) {
destFile := filepath.Join(t.TempDir(), "dynamic-file")
original := bytes.Repeat([]byte{'a'}, 64*1024)
updated := bytes.Clone(original)
updated[0] = 'b'

require.NoError(t, os.WriteFile(destFile, original, 0o600))
require.NoError(t, WriteIfChanged(destFile, updated, 0o600))
actual, err := os.ReadFile(destFile)
require.NoError(t, err)
assert.Equal(t, updated, actual,
"equal size and matching final 32 KB must not hide an earlier change")

// A rewrite is detected by an older modification time being lost, because
// the clock can tick too slowly to tell two writes apart.
past := time.Unix(1, 0)
require.NoError(t, os.Chtimes(destFile, past, past))
require.NoError(t, WriteIfChanged(destFile, updated, 0o600))
info, err := os.Stat(destFile)
require.NoError(t, err)
assert.Equal(t, past.UTC(), info.ModTime().UTC(), "matching contents should not be rewritten")
}
Loading
Loading