diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..3d78727 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,56 @@ +name: CI + +on: + # Run on pushes to main (e.g. merges) and on every PR. Limiting push to main + # avoids double runs on branches that also have an open PR. + push: + branches: [main] + pull_request: + +# Cancel superseded runs for the same branch/PR to save resources. +concurrency: + group: ci-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + lint: + name: Lint (gofmt + vet) + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + - uses: actions/setup-go@v5 + with: + go-version-file: go.mod + cache: true + - name: Check formatting + run: | + unformatted="$(gofmt -l .)" + if [ -n "$unformatted" ]; then + echo "The following files are not gofmt-formatted:" + echo "$unformatted" + echo "Run: gofmt -w ." + exit 1 + fi + - name: go vet + run: go vet ./... + + test: + name: Build & test + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, macos-latest, windows-latest] + steps: + - uses: actions/checkout@v6 + - uses: actions/setup-go@v5 + with: + go-version-file: go.mod + cache: true + - name: Build + run: go build ./... + - name: Test + run: go test -race ./... diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7235c47 --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +# Built binary +/php-debugger +/php-debugger.exe + +# Go build/test artifacts +*.test +*.out diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..341c690 --- /dev/null +++ b/go.mod @@ -0,0 +1,10 @@ +module github.com/php-debugger/installer + +go 1.25.6 + +require github.com/spf13/cobra v1.10.2 + +require ( + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/spf13/pflag v1.0.9 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..a6ee3e0 --- /dev/null +++ b/go.sum @@ -0,0 +1,10 @@ +github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= +github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4= +github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY= +github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/internal/cli/install.go b/internal/cli/install.go new file mode 100644 index 0000000..0100913 --- /dev/null +++ b/internal/cli/install.go @@ -0,0 +1,42 @@ +package cli + +import "github.com/spf13/cobra" + +// installOptions holds flags specific to the install command. +type installOptions struct { + // PHPVersion selects the PHP version to install (e.g. "8.3"); empty means + // the latest available in the release. Interpreter installs only. + PHPVersion string + // ZTS installs the thread-safe build; default is non-thread-safe. + // Interpreter installs only. + ZTS bool + // ExtensionOnly installs just the debugger extension into the current PHP + // instead of a full interpreter. + ExtensionOnly bool +} + +func newInstallCmd() *cobra.Command { + opts := &installOptions{} + + cmd := &cobra.Command{ + Use: "install", + Short: "Install the PHP debugger interpreter (default) or just the extension", + Long: "Install a self-contained PHP interpreter with the debugger compiled in\n" + + "(default), or, with --extension-only, install just the debugger extension\n" + + "into the currently active PHP.", + Args: cobra.NoArgs, + RunE: func(cmd *cobra.Command, args []string) error { + return errNotImplemented("install") + }, + } + + f := cmd.Flags() + f.StringVarP(&opts.PHPVersion, "php", "p", "", + "PHP version to install, e.g. 8.3 (default: latest available; interpreter only)") + f.BoolVarP(&opts.ZTS, "zts", "z", false, + "install the thread-safe (ZTS) build; default is non-thread-safe (interpreter only)") + f.BoolVarP(&opts.ExtensionOnly, "extension-only", "e", false, + "install only the debugger extension into the current PHP") + + return cmd +} diff --git a/internal/cli/root.go b/internal/cli/root.go new file mode 100644 index 0000000..b5ab37a --- /dev/null +++ b/internal/cli/root.go @@ -0,0 +1,64 @@ +package cli + +import ( + "fmt" + "os" + + "github.com/spf13/cobra" +) + +// GlobalOptions holds flags shared by every subcommand. +type GlobalOptions struct { + // User selects the no-sudo, per-user install scope instead of the default + // system-wide scope (/opt on Linux/macOS, Program Files on Windows). + User bool + // Yes assumes "yes" for every interactive confirmation (for CI). + Yes bool + // Verbose enables extra diagnostic output. + Verbose bool +} + +var globalOpts GlobalOptions + +func newRootCmd() *cobra.Command { + rootCmd := &cobra.Command{ + Use: "php-debugger", + Short: "Install and manage the PHP debugger", + Long: "php-debugger installs the PHP debugger from the php-debugger/php-debugger\n" + + "GitHub releases: either a self-contained PHP interpreter with the debugger\n" + + "compiled in (default), or the debugger extension for an existing PHP.", + SilenceUsage: true, + SilenceErrors: true, + } + + pf := rootCmd.PersistentFlags() + pf.BoolVarP(&globalOpts.User, "user", "u", false, + "install into a per-user directory (no sudo); default is system-wide") + pf.BoolVarP(&globalOpts.Yes, "yes", "y", false, + "assume yes for all prompts (non-interactive)") + pf.BoolVarP(&globalOpts.Verbose, "verbose", "V", false, + "enable verbose output") + + rootCmd.AddCommand( + newInstallCmd(), + newUpdateCmd(), + newUninstallCmd(), + newSwitchCmd(), + ) + + return rootCmd +} + +// Execute runs the root command and exits the process with an appropriate code. +func Execute() { + if err := newRootCmd().Execute(); err != nil { + fmt.Fprintln(os.Stderr, "Error:", err) + os.Exit(1) + } +} + +// errNotImplemented is returned by subcommand stubs until they are built out in +// later steps of the roadmap. +func errNotImplemented(name string) error { + return fmt.Errorf("%q is not implemented yet", name) +} diff --git a/internal/cli/switch.go b/internal/cli/switch.go new file mode 100644 index 0000000..f15e131 --- /dev/null +++ b/internal/cli/switch.go @@ -0,0 +1,30 @@ +package cli + +import "github.com/spf13/cobra" + +// switchOptions holds flags specific to the switch command. +type switchOptions struct { + // ZTS selects the thread-safe variant of the target version. + ZTS bool +} + +func newSwitchCmd() *cobra.Command { + opts := &switchOptions{} + + cmd := &cobra.Command{ + Use: "switch ", + Short: "Switch the active PHP version (installing it first if needed)", + Long: "Point the active PHP symlink at the given version (and optionally its\n" + + "thread-safe variant). If that variant is not installed yet, it is installed\n" + + "first.", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + return errNotImplemented("switch") + }, + } + + cmd.Flags().BoolVarP(&opts.ZTS, "zts", "z", false, + "select the thread-safe (ZTS) variant of the target version") + + return cmd +} diff --git a/internal/cli/uninstall.go b/internal/cli/uninstall.go new file mode 100644 index 0000000..431d4dd --- /dev/null +++ b/internal/cli/uninstall.go @@ -0,0 +1,34 @@ +package cli + +import "github.com/spf13/cobra" + +// uninstallOptions holds flags specific to the uninstall command. +type uninstallOptions struct { + // Extension uninstalls the debugger extension. + Extension bool + // Interpreter uninstalls a debugger interpreter (optionally a specific + // version given as a positional argument). + Interpreter bool +} + +func newUninstallCmd() *cobra.Command { + opts := &uninstallOptions{} + + cmd := &cobra.Command{ + Use: "uninstall [version]", + Short: "Uninstall the interpreter or extension, restoring any backup", + Long: "Remove a debugger interpreter (optionally a specific version) or the\n" + + "debugger extension. If a backup of a previously replaced interpreter exists,\n" + + "it is restored.", + Args: cobra.MaximumNArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + return errNotImplemented("uninstall") + }, + } + + f := cmd.Flags() + f.BoolVarP(&opts.Extension, "extension", "e", false, "uninstall the debugger extension") + f.BoolVarP(&opts.Interpreter, "interpreter", "i", false, "uninstall the debugger interpreter") + + return cmd +} diff --git a/internal/cli/update.go b/internal/cli/update.go new file mode 100644 index 0000000..9171162 --- /dev/null +++ b/internal/cli/update.go @@ -0,0 +1,33 @@ +package cli + +import "github.com/spf13/cobra" + +// updateOptions holds flags specific to the update command. +type updateOptions struct { + // Extension updates the installed debugger extension. + Extension bool + // Interpreter updates the active debugger interpreter. + Interpreter bool +} + +func newUpdateCmd() *cobra.Command { + opts := &updateOptions{} + + cmd := &cobra.Command{ + Use: "update", + Short: "Update the installed interpreter or extension to the latest release", + Long: "Re-install the currently active interpreter (or the extension) against the\n" + + "latest release. Use --extension or --interpreter to disambiguate when both\n" + + "are installed.", + Args: cobra.NoArgs, + RunE: func(cmd *cobra.Command, args []string) error { + return errNotImplemented("update") + }, + } + + f := cmd.Flags() + f.BoolVarP(&opts.Extension, "extension", "e", false, "update the debugger extension") + f.BoolVarP(&opts.Interpreter, "interpreter", "i", false, "update the debugger interpreter") + + return cmd +} diff --git a/main.go b/main.go new file mode 100644 index 0000000..90ce896 --- /dev/null +++ b/main.go @@ -0,0 +1,9 @@ +// Command php-debugger installs and manages the PHP debugger from the +// php-debugger/php-debugger GitHub releases. +package main + +import "github.com/php-debugger/installer/internal/cli" + +func main() { + cli.Execute() +}