diff --git a/.github/workflows/goreleaser-check.yml b/.github/workflows/goreleaser-check.yml new file mode 100644 index 0000000..bdd5a52 --- /dev/null +++ b/.github/workflows/goreleaser-check.yml @@ -0,0 +1,48 @@ +name: GoReleaser Check + +# Validates .goreleaser.yaml against a real snapshot build (cross-compile + +# archive generation, no publish) so config/toolchain regressions surface on +# the PR instead of after release-please has already cut a tag & GitHub +# Release. See release-please.yml's `publish` job, which runs the real +# `goreleaser release --clean` only after that release already exists. + +on: + pull_request: + branches: [ '**' ] + paths: + - '.goreleaser.yaml' + - '.github/workflows/goreleaser-check.yml' + - '.github/workflows/release-please.yml' + - 'go.mod' + - 'go.sum' + - 'version.go' + workflow_dispatch: + +jobs: + snapshot: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Install Go + uses: actions/setup-go@v5 + with: + go-version-file: go.mod + cache: true + + - name: Validate GoReleaser config + uses: goreleaser/goreleaser-action@v6 + with: + distribution: goreleaser + version: latest + args: check + + - name: Snapshot build (validates cross-compilation & archives) + uses: goreleaser/goreleaser-action@v6 + with: + distribution: goreleaser + version: latest + args: release --snapshot --clean diff --git a/.github/workflows/release-please.yml b/.github/workflows/release-please.yml new file mode 100644 index 0000000..de28c42 --- /dev/null +++ b/.github/workflows/release-please.yml @@ -0,0 +1,59 @@ +name: Release Please + +on: + push: + branches: + - master + workflow_dispatch: + +permissions: + contents: write + pull-requests: write + +jobs: + release-please: + runs-on: ubuntu-latest + outputs: + releases_created: ${{ steps.release.outputs.releases_created }} + tag_name: ${{ steps.release.outputs.tag_name }} + steps: + - uses: actions/checkout@v4 + + - uses: googleapis/release-please-action@v5 + id: release + with: + config-file: .release-please-config.json + manifest-file: .release-please-manifest.json + target-branch: master + + # Build & publish binaries only when release-please actually cut a release + # (i.e. a release-please PR was just merged to master). + publish: + needs: release-please + if: ${{ needs.release-please.outputs.releases_created == 'true' }} + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + ref: ${{ needs.release-please.outputs.tag_name }} + fetch-depth: 0 + + - name: Install Go + uses: actions/setup-go@v5 + with: + go-version-file: go.mod + cache: true + + - name: Run GoReleaser + uses: goreleaser/goreleaser-action@v6 + with: + distribution: goreleaser + version: latest + # release-please already created the GitHub Release (notes, tag); + # GoReleaser just builds & uploads the binary archives to it. + args: release --clean + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.gitignore b/.gitignore index 1f3a6eb..d5791e7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,6 @@ # IDE files .idea/ *.iml + +# goreleaser build output +/dist/ diff --git a/.goreleaser.yaml b/.goreleaser.yaml new file mode 100644 index 0000000..17a298d --- /dev/null +++ b/.goreleaser.yaml @@ -0,0 +1,51 @@ +version: 2 +project_name: gopy + +before: + hooks: + - go mod tidy + +builds: + - id: gopy + main: . + binary: gopy + env: + - CGO_ENABLED=0 + goos: [linux, darwin, windows] + goarch: [amd64, arm64] + ldflags: + - -s -w + - -X main.GitCommit={{ .ShortCommit }} + - -X 'main.VersionDate={{ .CommitDate }} UTC' + +archives: + - id: gopy + formats: [tar.gz] + format_overrides: + - goos: windows + formats: [zip] + name_template: >- + {{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }} + +checksum: + name_template: "checksums.txt" + +changelog: + sort: asc + filters: + exclude: + - "^docs:" + - "^test:" + - "^ci:" + - "^Merge pull request" + +release: + github: + owner: go-python + name: gopy + # release-please already created the GitHub Release for this tag (with its + # own changelog-derived notes); keep those notes as-is and just attach + # the built archives/checksums to it. + mode: keep-existing + prerelease: auto + name_template: "{{ .Tag }}" diff --git a/.release-please-config.json b/.release-please-config.json new file mode 100644 index 0000000..34189c0 --- /dev/null +++ b/.release-please-config.json @@ -0,0 +1,30 @@ +{ + "release-type": "go", + "include-v-in-tag": true, + "bootstrap-sha": "d09ff0f8254c8bd4a131157bf276c83330930cb5", + "packages": { + ".": { + "release-type": "go", + "changelog-path": "CHANGELOG.md", + "extra-files": [ + { + "path": "version.go", + "type": "generic" + } + ] + } + }, + "changelog-sections": [ + { "type": "feat", "section": "Features", "hidden": false }, + { "type": "fix", "section": "Bug Fixes", "hidden": false }, + { "type": "perf", "section": "Performance Improvements", "hidden": false }, + { "type": "revert", "section": "Reverts", "hidden": false }, + { "type": "docs", "section": "Documentation", "hidden": false }, + { "type": "style", "section": "Styles", "hidden": true }, + { "type": "chore", "section": "Miscellaneous Chores", "hidden": true }, + { "type": "refactor", "section": "Code Refactoring", "hidden": false }, + { "type": "test", "section": "Tests", "hidden": true }, + { "type": "build", "section": "Build System", "hidden": false }, + { "type": "ci", "section": "Continuous Integration", "hidden": true } + ] +} diff --git a/.release-please-manifest.json b/.release-please-manifest.json new file mode 100644 index 0000000..e7180db --- /dev/null +++ b/.release-please-manifest.json @@ -0,0 +1,3 @@ +{ + ".": "0.4.10" +} diff --git a/Makefile b/Makefile index 604892a..e4cdd8b 100644 --- a/Makefile +++ b/Makefile @@ -11,11 +11,15 @@ DIRS=`go list ./...` PYTHON=python3 PIP=$(PYTHON) -m pip +GIT_COMMIT=`git rev-parse --short HEAD` +VERS_DATE=`date -u +%Y-%m-%d\ %H:%M` +LDFLAGS=-X 'main.GitCommit=$(GIT_COMMIT)' -X 'main.VersionDate=$(VERS_DATE) UTC' + all: build -build: +build: @echo "GO111MODULE = $(value GO111MODULE)" - $(GOBUILD) -v $(DIRS) + $(GOBUILD) -v -ldflags "$(LDFLAGS)" $(DIRS) test: @echo "GO111MODULE = $(value GO111MODULE)" @@ -52,30 +56,7 @@ prereq: @echo " _PyInit__gi, referenced from:..." @echo - -# NOTE: MUST update version number here prior to running 'make release' and edit this file! -VERS=v0.4.10 -PACKAGE=main -GIT_COMMIT=`git rev-parse --short HEAD` -VERS_DATE=`date -u +%Y-%m-%d\ %H:%M` -VERS_FILE=version.go - -release: - /bin/rm -f $(VERS_FILE) - @echo "// WARNING: auto-generated by Makefile release target -- run 'make release' to update" > $(VERS_FILE) - @echo "" >> $(VERS_FILE) - @echo "package $(PACKAGE)" >> $(VERS_FILE) - @echo "" >> $(VERS_FILE) - @echo "const (" >> $(VERS_FILE) - @echo " Version = \"$(VERS)\"" >> $(VERS_FILE) - @echo " GitCommit = \"$(GIT_COMMIT)\" // the commit JUST BEFORE the release" >> $(VERS_FILE) - @echo " VersionDate = \"$(VERS_DATE)\" // UTC" >> $(VERS_FILE) - @echo ")" >> $(VERS_FILE) - @echo "" >> $(VERS_FILE) - goimports -w $(VERS_FILE) - /bin/cat $(VERS_FILE) - git commit -am "$(VERS) release" - git tag -a $(VERS) -m "$(VERS) release" - git push - git push origin --tags +# Releases are managed by release-please (.github/workflows/release-please.yml): +# merging its release PR to master bumps version.go, tags the commit, and +# publishes the GitHub Release. GoReleaser then builds & uploads binaries to it. diff --git a/bind/bind.go b/bind/bind.go index a3b7a85..93e78af 100644 --- a/bind/bind.go +++ b/bind/bind.go @@ -28,6 +28,9 @@ type BindCfg struct { PkgPrefix string // rename Go exported symbols to python PEP snake_case RenameCase bool + // gopy version string embedded in this binary, stamped into generated + // file headers so output can be traced back to the release that produced it + Version string } // ErrorList is a list of errors diff --git a/bind/gen.go b/bind/gen.go index fe96bd6..faa6e95 100644 --- a/bind/gen.go +++ b/bind/gen.go @@ -38,11 +38,11 @@ var WindowsOS = false // for all preambles: 1 = name of package (outname), 2 = cmdstr -// 3 = libcfg, 4 = GoHandle, 5 = CGoHandle, 6 = all imports, 7 = mainstr, 8 = exe pre C, 9 = exe pre go +// 3 = libcfg, 4 = GoHandle, 5 = CGoHandle, 6 = all imports, 7 = mainstr, 8 = exe pre C, 9 = exe pre go, 10 = gopy version const ( goPreamble = `/* cgo stubs for package %[1]s. -File is generated by gopy. Do not edit. +File is generated by gopy version %[10]s. Do not edit. %[2]s */ @@ -263,8 +263,9 @@ func GoPyMainRun() { ` + // 3 = gopy version PyBuildPreamble = `# python build stubs for package %[1]s -# File is generated by gopy. Do not edit. +# File is generated by gopy version %[3]s. Do not edit. # %[2]s from pybindgen import retval, param, Function, Module @@ -317,11 +318,11 @@ mod.add_function('_gopy_clear_go_tls', None, []) // appended to imports in py wrap preamble as key for adding at end importHereKeyString = "%%%%%%<<<<<>>>>>>%%%%%%%" - // 3 = specific package name, 4 = spec pkg path, 5 = doc, 6 = imports + // 3 = specific package name, 4 = spec pkg path, 5 = doc, 6 = imports, 8 = gopy version PyWrapPreamble = `%[5]s # python wrapper for package %[4]s within overall package %[1]s # This is what you import to use the package. -# File is generated by gopy. Do not edit. +# File is generated by gopy version %[8]s. Do not edit. # %[2]s # the following is required to enable dlopen to open the _go.so file @@ -379,11 +380,11 @@ except Exception: ` // exe version of preamble -- doesn't need complex code to load _ module - // 3 = specific package name, 4 = spec pkg path, 5 = doc, 6 = imports + // 3 = specific package name, 4 = spec pkg path, 5 = doc, 6 = imports, 8 = gopy version PyWrapExePreamble = `%[5]s # python wrapper for package %[4]s within standalone executable package %[1]s # This is what you import to use the package. -# File is generated by gopy. Do not edit. +# File is generated by gopy version %[8]s. Do not edit. # %[2]s import collections @@ -431,9 +432,9 @@ def Init(): ` // 3 = gencmd, 4 = vm, 5 = libext 6 = extraGccArgs, 7 = CFLAGS, 8 = LDLFAGS, - // 9 = windows special declspec hack + // 9 = windows special declspec hack, 10 = gopy version MakefileTemplate = `# Makefile for python interface for package %[1]s. -# File is generated by gopy. Do not edit. +# File is generated by gopy version %[10]s. Do not edit. # %[2]s GOCMD=go @@ -470,9 +471,9 @@ build: ` - // exe version of template: 3 = gencmd, 4 = vm, 5 = libext + // exe version of template: 3 = gencmd, 4 = vm, 5 = libext, 8 = gopy version MakefileExeTemplate = `# Makefile for python interface for standalone executable package %[1]s. -# File is generated by gopy. Do not edit. +# File is generated by gopy version %[8]s. Do not edit. # %[2]s GOCMD=go @@ -721,12 +722,12 @@ func (g *pyGen) genGoPreamble() { exeprego = goExePreambleGo } g.gofile.Printf(goPreamble, g.cfg.Name, g.cfg.Cmd, libcfg, GoHandle, CGoHandle, - pkgimport, g.cfg.Main, exeprec, exeprego) + pkgimport, g.cfg.Main, exeprec, exeprego, g.cfg.Version) g.gofile.Printf("\n// --- generated code for package: %[1]s below: ---\n\n", g.cfg.Name) } func (g *pyGen) genPyBuildPreamble() { - g.pybuild.Printf(PyBuildPreamble, g.cfg.Name, g.cfg.Cmd) + g.pybuild.Printf(PyBuildPreamble, g.cfg.Name, g.cfg.Cmd, g.cfg.Version) } func (g *pyGen) genPyWrapPreamble() { @@ -784,9 +785,9 @@ func (g *pyGen) genPyWrapPreamble() { impstr += importHereKeyString if g.mode == ModeExe { - g.pywrap.Printf(PyWrapExePreamble, g.cfg.Name, g.cfg.Cmd, n, pkgimport, pkgDoc, impgenstr, impstr) + g.pywrap.Printf(PyWrapExePreamble, g.cfg.Name, g.cfg.Cmd, n, pkgimport, pkgDoc, impgenstr, impstr, g.cfg.Version) } else { - g.pywrap.Printf(PyWrapPreamble, g.cfg.Name, g.cfg.Cmd, n, pkgimport, pkgDoc, impgenstr, impstr) + g.pywrap.Printf(PyWrapPreamble, g.cfg.Name, g.cfg.Cmd, n, pkgimport, pkgDoc, impgenstr, impstr, g.cfg.Version) } } @@ -816,14 +817,14 @@ func (g *pyGen) genMakefile() { } if g.mode == ModeExe { - g.makefile.Printf(MakefileExeTemplate, g.cfg.Name, g.cfg.Cmd, gencmd, g.cfg.VM, g.libext, pycfg.CFlags, pycfg.LdFlags) + g.makefile.Printf(MakefileExeTemplate, g.cfg.Name, g.cfg.Cmd, gencmd, g.cfg.VM, g.libext, pycfg.CFlags, pycfg.LdFlags, g.cfg.Version) } else { winhack := "" if WindowsOS { winhack = fmt.Sprintf(`# windows-only sed hack here to fix pybindgen declaration of PyInit sed -i "s/ PyInit_/ __declspec(dllexport) PyInit_/g" %s.c`, g.cfg.Name) } - g.makefile.Printf(MakefileTemplate, g.cfg.Name, g.cfg.Cmd, gencmd, g.cfg.VM, g.libext, g.extraGccArgs, pycfg.CFlags, pycfg.LdFlags, winhack) + g.makefile.Printf(MakefileTemplate, g.cfg.Name, g.cfg.Cmd, gencmd, g.cfg.VM, g.libext, g.extraGccArgs, pycfg.CFlags, pycfg.LdFlags, winhack, g.cfg.Version) } } diff --git a/cmd_version.go b/cmd_version.go new file mode 100644 index 0000000..0419dc4 --- /dev/null +++ b/cmd_version.go @@ -0,0 +1,32 @@ +// Copyright 2026 The go-python Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package main + +import ( + "fmt" + + "github.com/gonuts/commander" + "github.com/gonuts/flag" +) + +func gopyMakeCmdVersion() *commander.Command { + return &commander.Command{ + Run: gopyRunCmdVersion, + UsageLine: "version", + Short: "print gopy version information", + Long: ` +version prints the gopy version, git commit, and build date embedded in this binary at release time. + +ex: + $ gopy version +`, + Flag: *flag.NewFlagSet("gopy-version", flag.ExitOnError), + } +} + +func gopyRunCmdVersion(cmdr *commander.Command, args []string) error { + fmt.Printf("gopy version %s (commit %s, built %s)\n", Version, GitCommit, VersionDate) + return nil +} diff --git a/main.go b/main.go index e59f56f..9e968ca 100644 --- a/main.go +++ b/main.go @@ -38,6 +38,7 @@ type BuildCfg struct { func NewBuildCfg() *BuildCfg { var cfg BuildCfg cfg.Cmd = argStr() + cfg.Version = Version return &cfg } @@ -49,6 +50,7 @@ func run(args []string) error { gopyMakeCmdBuild(), gopyMakeCmdPkg(), gopyMakeCmdExe(), + gopyMakeCmdVersion(), }, Flag: *flag.NewFlagSet("gopy", flag.ExitOnError), } diff --git a/main_test.go b/main_test.go index 1127698..dd1ad54 100644 --- a/main_test.go +++ b/main_test.go @@ -128,6 +128,77 @@ ignoring python incompatible function: .func github.com/go-python/gopy/_examples } } +func TestVersion(t *testing.T) { + cmd := exec.Command("go", "run", ".", "version") + out, err := cmd.CombinedOutput() + if err != nil { + t.Fatalf("could not run %v: %+v\n%s", strings.Join(cmd.Args, " "), err, out) + } + got := strings.TrimSpace(string(out)) + want := fmt.Sprintf("gopy version %s (commit %s, built %s)", Version, GitCommit, VersionDate) + if got != want { + t.Fatalf("gopy version = %q, want %q", got, want) + } +} + +func TestVersionLdflags(t *testing.T) { + workdir, err := os.MkdirTemp("", "gopy-version-ldflags-") + if err != nil { + t.Fatalf("could not create workdir: %v\n", err) + } + defer os.RemoveAll(workdir) + + binPath := filepath.Join(workdir, "gopy-versioned") + const ( + wantCommit = "deadbeef" + wantDate = "2026-01-02 03:04 UTC" + ) + ldflags := fmt.Sprintf("-X 'main.GitCommit=%s' -X 'main.VersionDate=%s'", wantCommit, wantDate) + + build := exec.Command("go", "build", "-ldflags", ldflags, "-o", binPath, ".") + if out, err := build.CombinedOutput(); err != nil { + t.Fatalf("could not build with ldflags: %+v\n%s", err, out) + } + + out, err := exec.Command(binPath, "version").CombinedOutput() + if err != nil { + t.Fatalf("could not run %s version: %+v\n%s", binPath, err, out) + } + got := strings.TrimSpace(string(out)) + want := fmt.Sprintf("gopy version %s (commit %s, built %s)", Version, wantCommit, wantDate) + if got != want { + t.Fatalf("gopy version (ldflags-stamped) = %q, want %q", got, want) + } +} + +func TestGenHeaderHasVersion(t *testing.T) { + pyvm := testBackends["py3"] + workdir, err := os.MkdirTemp("", "gopy-") + if err != nil { + t.Fatalf("could not create workdir: %v\n", err) + } + defer os.RemoveAll(workdir) + + curPkgPath := reflect.TypeOf(pkg{}).PkgPath() + fpath := filepath.Join(curPkgPath, "_examples/hi") + cmd := exec.Command("go", "run", ".", "gen", "-vm="+pyvm, "-output="+workdir, fpath) + out, err := cmd.CombinedOutput() + if err != nil { + t.Fatalf("could not run %v: %+v\n%s", strings.Join(cmd.Args, " "), err, out) + } + + want := fmt.Sprintf("generated by gopy version %s.", Version) + for _, fname := range []string{"hi.go", "build.py", "Makefile", "hi.py"} { + b, err := os.ReadFile(filepath.Join(workdir, fname)) + if err != nil { + t.Fatalf("could not read generated %s: %v", fname, err) + } + if !strings.Contains(string(b), want) { + t.Errorf("%s does not contain %q:\n%s", fname, want, string(b)) + } + } +} + func TestHi(t *testing.T) { // t.Parallel() path := "_examples/hi" diff --git a/version.go b/version.go index 55a0f48..efc2065 100644 --- a/version.go +++ b/version.go @@ -1,9 +1,13 @@ -// WARNING: auto-generated by Makefile release target -- run 'make release' to update +// Version is bumped by release-please via the x-release-please-version +// marker below; do not hand-edit the value on that line. +// +// GitCommit and VersionDate are stamped at build time via `go build -ldflags` +// (see Makefile and .goreleaser.yaml), so they default to "unknown" here. package main -const ( - Version = "v0.4.10" - GitCommit = "b735a58" // the commit JUST BEFORE the release - VersionDate = "2024-05-03 22:57" // UTC +var ( + Version = "v0.4.10" // x-release-please-version + GitCommit = "unknown" + VersionDate = "unknown" )