diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..63753c2 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,42 @@ +name: goreleaser + +on: + push: + tags: + - 'v*' + +permissions: + contents: write + +jobs: + goreleaser: + 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: Verify tag matches version.go + run: | + TAG="${GITHUB_REF#refs/tags/}" + VERS=$(grep -oP 'Version\s*=\s*"\K[^"]+' version.go) + if [ "$TAG" != "$VERS" ]; then + echo "::error::pushed tag $TAG does not match version.go Version=$VERS" + exit 1 + fi + + - name: Run GoReleaser + uses: goreleaser/goreleaser-action@v6 + with: + distribution: goreleaser + version: latest + 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..a3e08a2 --- /dev/null +++ b/.goreleaser.yaml @@ -0,0 +1,45 @@ +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 + +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 + prerelease: auto + name_template: "{{ .Tag }}" diff --git a/Makefile b/Makefile index 604892a..d8ef8a3 100644 --- a/Makefile +++ b/Makefile @@ -77,5 +77,7 @@ release: git commit -am "$(VERS) release" git tag -a $(VERS) -m "$(VERS) release" git push + # pushing the tag triggers .github/workflows/release.yml, which builds and + # publishes the GitHub Release via goreleaser git push origin --tags 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 f6d8604..75cc3de 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 */ @@ -257,8 +257,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 @@ -311,11 +312,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 @@ -373,11 +374,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 @@ -425,9 +426,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 @@ -464,9 +465,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 @@ -715,12 +716,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() { @@ -778,9 +779,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) } } @@ -810,14 +811,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..0e76267 100644 --- a/main_test.go +++ b/main_test.go @@ -128,6 +128,47 @@ 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 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"