diff --git a/.gitmodules b/.gitmodules deleted file mode 100644 index e1a3ca85..00000000 --- a/.gitmodules +++ /dev/null @@ -1,3 +0,0 @@ -[submodule "ext/static-php-cli"] - path = ext/static-php-cli - url = https://github.com/crazywhalecc/static-php-cli diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 00000000..478b0fd1 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,160 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +The Upsun CLI is a Go-based command-line interface for Upsun (formerly Platform.sh). The CLI is a hybrid system that wraps a legacy PHP CLI while providing new Go-based commands. It supports multiple vendors through build tags and configuration files. + +## Build and Test Commands + +Build a single binary for your platform: +```bash +make single +``` + +Build a snapshot for all platforms: +```bash +make snapshot +``` + +Run tests: +```bash +make test +# or directly: +GOEXPERIMENT=jsonv2 go test -v -race -cover -count=1 ./... +``` + +Run linters: +```bash +make lint +# or individual linters: +make lint-gomod +make lint-golangci +``` + +Format code: +```bash +go fmt ./... +``` + +Tidy dependencies: +```bash +go mod tidy +``` + +Run a single test: +```bash +go test -v -run TestName ./path/to/package +``` + +## Architecture + +### Hybrid CLI System + +The CLI operates as a wrapper around a legacy PHP CLI: +- Go layer: Handles new commands (init, list, version, config:install, project:convert) and core infrastructure +- PHP layer: Legacy commands are proxied through `internal/legacy/CLIWrapper` +- The PHP CLI (platform.phar) is embedded at build time via go:embed + +### Key Components + +**Entry Point**: `cmd/platform/main.go` +- Loads configuration from YAML (embedded or external) +- Sets up Viper for environment variable handling +- Delegates to commands package + +**Commands**: `commands/` +- `root.go`: Root command that sets up the Cobra CLI and delegates to legacy CLI when needed +- Native Go commands: init, list, version, config:install, project:convert, completion +- Unrecognized commands are passed to the legacy PHP CLI + +**Configuration**: `internal/config/` +- `schema.go`: Config struct definition with validation tags +- Supports vendorization through embedded YAML configs (config_upsun.go, config_platformsh.go, config_vendor.go) +- Uses build tags to select which config is embedded +- Config can be loaded from external files for testing/development + +**Legacy Integration**: `internal/legacy/` +- `legacy.go`: CLIWrapper that manages PHP binary and phar execution +- PHP binaries are embedded per platform via go:embed and build tags +- Uses file locking to prevent concurrent initialization +- Copies PHP binary and phar to cache directory on first run + +**API Client**: `internal/api/` +- HTTP client for interacting with Platform.sh/Upsun API +- Handles authentication, organizations, and resource management + +**Authentication**: `internal/auth/` +- JWT handling and OAuth2 flow +- Custom transport for API authentication + +**Project Initialization**: `internal/init/` +- AI-powered project configuration generation +- Integrates with whatsun library for codebase analysis + +### Build System + +**Multi-Vendor Support**: +- Uses Go build tags (platform, upsun, vendor) to compile different binaries +- Configuration is embedded at compile time +- GoReleaser builds multiple variants (platform, upsun, vendor-specific) + +**PHP Binary Handling**: +- PHP binaries are downloaded from [upsun/cli-php-builds](https://github.com/upsun/cli-php-builds) releases +- 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) + +**Downloading PHP Binaries**: +```bash +# Download PHP for current platform only (for development) +make php + +# Download all PHP binaries (for release builds) +make download-php +``` + +**Upgrading PHP Version**: +1. Trigger the build workflow at [upsun/cli-php-builds](https://github.com/upsun/cli-php-builds/actions) with the new PHP version +2. Update `PHP_VERSION` in the Makefile +3. Run `make php` to download the new binary +4. Test and release + +## Development Notes + +### Testing + +Tests use github.com/stretchr/testify for assertions. Table-driven tests are preferred with a "cases" slice containing simple test case structs. + +### Configuration + +The CLI uses Viper for configuration. Environment variables use the prefix defined in the config (UPSUN_CLI_ or PLATFORM_CLI_). The prefix is set in the config YAML. + +### Legacy CLI Interaction + +When the root command receives arguments it doesn't recognize, it passes them to the legacy PHP CLI via CLIWrapper.Exec(). The PHP binary and phar are extracted to a cache directory on first use. + +### Vendorization + +To build a vendor-specific CLI: +```bash +make vendor-snapshot VENDOR_NAME='Vendor Name' VENDOR_BINARY='vendorcli' +make vendor-release VENDOR_NAME='Vendor Name' VENDOR_BINARY='vendorcli' +``` + +This requires a config file at `internal/config/embedded-config.yaml` (downloaded at build time). + +### Version Information + +Version information is injected at build time via ldflags: +- `internal/config.Version`: Git tag/version +- `internal/config.Commit`: Git commit hash +- `internal/config.Date`: Build date +- `internal/legacy.PHPVersion`: PHP version embedded +- `internal/legacy.LegacyCLIVersion`: Legacy CLI version embedded + +### Update Checks + +The CLI checks for updates from GitHub releases (when Wrapper.GitHubRepo is set in config). This runs in a background goroutine and prints a message after command execution. diff --git a/Dockerfile.php b/Dockerfile.php deleted file mode 100755 index cae04c1c..00000000 --- a/Dockerfile.php +++ /dev/null @@ -1,80 +0,0 @@ -FROM alpine:3.16 - -# define script basic information -# Version of this Dockerfile -ENV SCRIPT_VERSION=1.5.1 -# Download address uses backup address - -ARG USE_BACKUP_ADDRESS -ARG PHP_VERSION - -# (if downloading slowly, consider set it to yes) -ENV USE_BACKUP="${USE_BACKUP_ADDRESS}" - -# APK repositories mirror address, if u r not in China, consider set USE_BACKUP=yes to boost -ENV LINK_APK_REPO='mirrors.ustc.edu.cn' -ENV LINK_APK_REPO_BAK='dl-cdn.alpinelinux.org' - -RUN if [ "${USE_BACKUP}" = "" ]; then \ - export USE_BACKUP="no" ; \ - fi - -RUN if [ "${USE_BACKUP}" = "yes" ]; then \ - echo "Using backup original address..." ; \ - else \ - echo "Using mirror address..." && \ - sed -i 's/dl-cdn.alpinelinux.org/'${LINK_APK_REPO}'/g' /etc/apk/repositories ; \ - fi - -# build requirements -RUN apk add bash file wget cmake gcc g++ jq autoconf git libstdc++ linux-headers make m4 libgcc binutils ncurses dialog > /dev/null -# php zlib dependencies -RUN apk add zlib-dev zlib-static > /dev/null -# php mbstring dependencies -RUN apk add oniguruma-dev > /dev/null -# php openssl dependencies -RUN apk add openssl-libs-static openssl-dev openssl > /dev/null -# php gd dependencies -RUN apk add libpng-dev libpng-static > /dev/null -# curl c-ares dependencies -RUN apk add c-ares-static c-ares-dev > /dev/null -# php event dependencies -RUN apk add libevent libevent-dev libevent-static > /dev/null -# php sqlite3 dependencies -RUN apk add sqlite sqlite-dev sqlite-libs sqlite-static > /dev/null -# php libzip dependencies -RUN apk add bzip2-dev bzip2-static bzip2 > /dev/null -# php micro ffi dependencies -RUN apk add libffi libffi-dev > /dev/null -# php gd event parent dependencies -RUN apk add zstd-static > /dev/null -# php readline dependencies -RUN apk add readline-static ncurses-static readline-dev > /dev/null - -RUN mkdir /app - -WORKDIR /app - -COPY ./* /app/ - -RUN chmod +x /app/*.sh - -RUN ./download.sh swoole ${USE_BACKUP} && \ - ./download.sh inotify ${USE_BACKUP} && \ - ./download.sh mongodb ${USE_BACKUP} && \ - ./download.sh event ${USE_BACKUP} && \ - ./download.sh redis ${USE_BACKUP} && \ - ./download.sh libxml2 ${USE_BACKUP} && \ - ./download.sh xz ${USE_BACKUP} && \ - ./download.sh curl ${USE_BACKUP} && \ - ./download.sh libzip ${USE_BACKUP} && \ - ./download-git.sh dixyes/phpmicro phpmicro ${USE_BACKUP} - -RUN ./compile-deps.sh -RUN echo -e "#!/usr/bin/env bash\n/app/compile-php.sh \$@" > /bin/build-php && chmod +x /bin/build-php - -RUN /bin/build-php no-mirror $PHP_VERSION all /dist - -FROM scratch -ARG GOARCH -COPY --from=0 /dist/php /php_linux_$GOARCH diff --git a/Makefile b/Makefile index 99abc6e6..411fc355 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -PHP_VERSION = 8.2.29 +PHP_VERSION = 8.4.16 LEGACY_CLI_VERSION = 4.28.2 GORELEASER_ID ?= upsun @@ -7,10 +7,6 @@ ifeq ($(GOOS), darwin) GORELEASER_ID=$(GORELEASER_ID)-macos endif -# The OpenSSL version must be compatible with the PHP version. -# See: https://www.php.net/manual/en/openssl.requirements.php -OPENSSL_VERSION = 1.1.1t - GOOS := $(shell uname -s | tr '[:upper:]' '[:lower:]') GOARCH := $(shell uname -m) ifeq ($(GOARCH), x86_64) @@ -20,51 +16,55 @@ ifeq ($(GOARCH), aarch64) GOARCH=arm64 endif -PHP_BINARY_PATH := internal/legacy/archives/php_$(GOOS)_$(GOARCH) VERSION := $(shell git describe --always) # Tooling versions GORELEASER_VERSION=v2.12.0 -internal/legacy/archives/platform.phar: - curl -L https://github.com/platformsh/legacy-cli/releases/download/v$(LEGACY_CLI_VERSION)/platform.phar -o internal/legacy/archives/platform.phar +# PHP binaries are downloaded from cli-php-builds releases. +# See: https://github.com/upsun/cli-php-builds +PHP_BUILDS_REPO = upsun/cli-php-builds +PHP_RELEASE_URL = https://github.com/$(PHP_BUILDS_REPO)/releases/download/php-$(PHP_VERSION) -internal/legacy/archives/php_windows_amd64: internal/legacy/archives/php_windows.zip internal/legacy/archives/cacert.pem +internal/legacy/archives/platform.phar: + mkdir -p internal/legacy/archives + curl -fSL https://github.com/platformsh/legacy-cli/releases/download/v$(LEGACY_CLI_VERSION)/platform.phar -o internal/legacy/archives/platform.phar +# Download PHP binary for the current platform. internal/legacy/archives/php_darwin_$(GOARCH): - bash build-php-brew.sh $(GOOS) $(PHP_VERSION) $(OPENSSL_VERSION) - mv -f $(GOOS)/php-$(PHP_VERSION)/sapi/cli/php $(PHP_BINARY_PATH) - rm -rf $(GOOS) + mkdir -p internal/legacy/archives + curl -fSL "$(PHP_RELEASE_URL)/php-$(PHP_VERSION)-darwin-$(GOARCH)" -o $@ + chmod +x $@ internal/legacy/archives/php_linux_$(GOARCH): - cp ext/extensions.txt ext/static-php-cli/docker - docker buildx build \ - --build-arg GOARCH=$(GOARCH) \ - --build-arg PHP_VERSION=$(PHP_VERSION) \ - --build-arg USE_BACKUP_ADDRESS=yes \ - --file=./Dockerfile.php \ - --platform=linux/$(GOARCH) \ - --output=type=local,dest=./internal/legacy/archives/ \ - --progress=plain \ - ext/static-php-cli/docker - -PHP_WINDOWS_REMOTE_FILENAME := "php-$(PHP_VERSION)-nts-Win32-vs16-x64.zip" -internal/legacy/archives/php_windows.zip: - ( \ - set -e ;\ - mkdir -p internal/legacy/archives ;\ - cd internal/legacy/archives ;\ - curl -f "https://windows.php.net/downloads/releases/$(PHP_WINDOWS_REMOTE_FILENAME)" > php_windows.zip ;\ - curl -f https://windows.php.net/downloads/releases/sha256sum.txt | grep "$(PHP_WINDOWS_REMOTE_FILENAME)" | sed s/"$(PHP_WINDOWS_REMOTE_FILENAME)"/"php_windows.zip"/g > php_windows.zip.sha256 ;\ - sha256sum -c php_windows.zip.sha256 ;\ - ) + mkdir -p internal/legacy/archives + curl -fSL "$(PHP_RELEASE_URL)/php-$(PHP_VERSION)-linux-$(GOARCH)" -o $@ + chmod +x $@ + +internal/legacy/archives/php_windows_amd64: internal/legacy/archives/php_windows.exe internal/legacy/archives/cacert.pem + +internal/legacy/archives/php_windows.exe: + mkdir -p internal/legacy/archives + curl -fSL "$(PHP_RELEASE_URL)/php-$(PHP_VERSION)-windows-amd64.exe" -o $@ .PHONY: internal/legacy/archives/cacert.pem internal/legacy/archives/cacert.pem: mkdir -p internal/legacy/archives - curl https://curl.se/ca/cacert.pem > internal/legacy/archives/cacert.pem + curl -fSL https://curl.se/ca/cacert.pem -o internal/legacy/archives/cacert.pem -php: $(PHP_BINARY_PATH) +# Download all PHP binaries (for release builds). +.PHONY: download-php +download-php: + mkdir -p internal/legacy/archives + curl -fSL "$(PHP_RELEASE_URL)/php-$(PHP_VERSION)-linux-amd64" -o internal/legacy/archives/php_linux_amd64 + curl -fSL "$(PHP_RELEASE_URL)/php-$(PHP_VERSION)-linux-arm64" -o internal/legacy/archives/php_linux_arm64 + curl -fSL "$(PHP_RELEASE_URL)/php-$(PHP_VERSION)-darwin-amd64" -o internal/legacy/archives/php_darwin_amd64 + curl -fSL "$(PHP_RELEASE_URL)/php-$(PHP_VERSION)-darwin-arm64" -o internal/legacy/archives/php_darwin_arm64 + curl -fSL "$(PHP_RELEASE_URL)/php-$(PHP_VERSION)-windows-amd64.exe" -o internal/legacy/archives/php_windows.exe + curl -fSL https://curl.se/ca/cacert.pem -o internal/legacy/archives/cacert.pem + chmod +x internal/legacy/archives/php_linux_* internal/legacy/archives/php_darwin_* + +php: internal/legacy/archives/php_$(GOOS)_$(GOARCH) .PHONY: goreleaser goreleaser: diff --git a/build-php-brew.sh b/build-php-brew.sh deleted file mode 100644 index f7a3a702..00000000 --- a/build-php-brew.sh +++ /dev/null @@ -1,44 +0,0 @@ -set -ex - -DIR=$1 -PHP_VERSION=$2 -OPENSSL_VERSION=$3 - -brew install bison pkg-config coreutils autoconf - -SSL_DIR_PATH=$(pwd)/"$DIR"/ssl -mkdir -p "$SSL_DIR_PATH" - -curl -LfSsl https://www.openssl.org/source/openssl-"$OPENSSL_VERSION".tar.gz | tar xzf - -C "$DIR" -cd "$DIR"/openssl-"$OPENSSL_VERSION" - -./config no-shared --prefix="$SSL_DIR_PATH" --openssldir="$SSL_DIR_PATH" -make -make install - -cd ../.. -curl -fSsl https://www.php.net/distributions/php-"$PHP_VERSION".tar.gz | tar xzf - -C "$DIR" -cd "$DIR"/php-"$PHP_VERSION" - -rm -f sapi/cli/php - -./buildconf --force -./configure \ - --disable-shared \ - --enable-embed=static \ - --enable-filter \ - --enable-pcntl \ - --enable-phar \ - --enable-posix \ - --enable-static \ - --enable-sysvmsg \ - --with-curl \ - --with-openssl \ - --with-pear=no \ - --without-pcre-jit \ - --with-zlib \ - --disable-all \ -OPENSSL_CFLAGS="-I$SSL_DIR_PATH/include" \ -OPENSSL_LIBS="-L$SSL_DIR_PATH/lib -lssl -lcrypto" - -make -j"$(nproc)" cli diff --git a/ext/extensions.txt b/ext/extensions.txt deleted file mode 100755 index 691b457e..00000000 --- a/ext/extensions.txt +++ /dev/null @@ -1,44 +0,0 @@ -# Start with '#' is comments -# Start with '^' is deselecting extensions, which is not installed as default -# Each line just leave the extension name or ^ character - -^bcmath -^calendar -^ctype -curl -^dom -^event -^exif -^fileinfo -filter -^gd -^hash -^iconv -^inotify -^json -^libxml -^mbstring -^mongodb -^mysqlnd -openssl -pcntl -^pdo -^pdo_mysql -^pdo_sqlite -phar -posix -^protobuf -^readline -^redis -^shmop -^simplexml -^soap -^sockets -^sqlite3 -^swoole -^tokenizer -^xml -^xmlreader -^xmlwriter -zlib -^zip diff --git a/ext/static-php-cli b/ext/static-php-cli deleted file mode 160000 index 4c55f4a2..00000000 --- a/ext/static-php-cli +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 4c55f4a22be0b2e130af023ef7593bcefa78314a diff --git a/internal/legacy/php_manager_windows.go b/internal/legacy/php_manager_windows.go index 5b33043d..54d035a6 100644 --- a/internal/legacy/php_manager_windows.go +++ b/internal/legacy/php_manager_windows.go @@ -1,102 +1,32 @@ package legacy import ( - "archive/zip" - "bytes" _ "embed" - "fmt" - "io" - "os" "path/filepath" - "runtime" - "strings" - - "golang.org/x/sync/errgroup" "github.com/platformsh/cli/internal/file" ) -//go:embed archives/php_windows.zip +//go:embed archives/php_windows.exe var phpCLI []byte //go:embed archives/cacert.pem var caCert []byte func (m *phpManagerPerOS) copy() error { - destDir := filepath.Join(m.cacheDir, "php") - - r, err := zip.NewReader(bytes.NewReader(phpCLI), int64(len(phpCLI))) - if err != nil { - return fmt.Errorf("could not open zip reader: %w", err) - } - - g := errgroup.Group{} - g.SetLimit(runtime.GOMAXPROCS(0)) - for _, f := range r.File { - g.Go(func() error { - return copyZipFile(f, destDir) - }) - } - if err := g.Wait(); err != nil { + if err := file.WriteIfNeeded(m.binPath(), phpCLI, 0o755); err != nil { return err } - - if err := file.WriteIfNeeded(filepath.Join(destDir, "extras", "cacert.pem"), caCert, 0o644); err != nil { - return err - } - - return nil + // Write cacert.pem for OpenSSL CA bundle (Windows needs this explicitly). + return file.WriteIfNeeded(filepath.Join(m.cacheDir, "cacert.pem"), caCert, 0o644) } func (m *phpManagerPerOS) binPath() string { - return filepath.Join(m.cacheDir, "php", "php.exe") + return filepath.Join(m.cacheDir, "php.exe") } func (m *phpManagerPerOS) settings() []string { return []string{ - "extension=" + filepath.Join(m.cacheDir, "php", "ext", "php_curl.dll"), - "extension=" + filepath.Join(m.cacheDir, "php", "ext", "php_openssl.dll"), - "openssl.cafile=" + filepath.Join(m.cacheDir, "php", "extras", "cacert.pem"), - } -} - -// copyZipFile extracts a file from the Zip to the destination directory. -// If the file already exists and has the correct size, it will be skipped. -func copyZipFile(f *zip.File, destDir string) error { - destPath := filepath.Join(destDir, f.Name) - if !strings.HasPrefix(destPath, filepath.Clean(destDir)+string(os.PathSeparator)) { - return fmt.Errorf("invalid file path: %s", destPath) + "openssl.cafile=" + filepath.Join(m.cacheDir, "cacert.pem"), } - - if f.FileInfo().IsDir() { - if err := os.MkdirAll(destPath, 0755); err != nil { - return fmt.Errorf("could not create extracted directory %s: %w", destPath, err) - } - return nil - } - - if existingFileInfo, err := os.Lstat(destPath); err == nil && uint64(existingFileInfo.Size()) == f.UncompressedSize64 { - return nil - } - - if err := os.MkdirAll(filepath.Dir(destPath), 0755); err != nil { - return fmt.Errorf("could not create parent directory for extracted file %s: %w", destPath, err) - } - - rc, err := f.Open() - if err != nil { - return fmt.Errorf("could not open file in zip archive %s: %w", f.Name, err) - } - defer rc.Close() - - b, err := io.ReadAll(rc) - if err != nil { - return fmt.Errorf("could not extract zipped file %s: %w", f.Name, err) - } - - if err := file.Write(destPath, b, f.Mode()); err != nil { - return fmt.Errorf("could not copy extracted file %s: %w", destPath, err) - } - - return nil }