feat(veans): build and publish veans alongside vikunja
Cross-compile veans for the same OS/arch matrix as the main vikunja binary, wrap each into a signed zip, build deb/rpm/apk/archlinux packages via nfpm, and merge those into the existing dl.vikunja.io package repos so `apt install veans` works from the same source. - veans/magefile.go: Release namespace (xgo cross-compile, upx, sha256, per-target zip bundle, nfpm.yaml templating). - veans/nfpm.yaml: minimal — binary at /usr/local/bin/veans, no service or postinstall. - .github/workflows/release.yml: veans-binaries + veans-os-package jobs, veans artifacts merged into publish-repos and create-release. S3 layout mirrors vikunja under /veans/<version>/.
This commit is contained in:
parent
d5ab54941f
commit
5f00fca166
|
|
@ -141,6 +141,77 @@ jobs:
|
||||||
name: vikunja_bin_packages
|
name: vikunja_bin_packages
|
||||||
path: ./dist/zip/*
|
path: ./dist/zip/*
|
||||||
|
|
||||||
|
veans-binaries:
|
||||||
|
runs-on: blacksmith-8vcpu-ubuntu-2204
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
|
||||||
|
- name: Git describe
|
||||||
|
id: ghd
|
||||||
|
uses: proudust/gh-describe@v2
|
||||||
|
- uses: useblacksmith/setup-go@647ac649bd5b480f2a262e3e3e5f4d150ed452ad # v6
|
||||||
|
with:
|
||||||
|
go-version: stable
|
||||||
|
- name: Install mage
|
||||||
|
# The cached mage-static artifact has the parent magefile compiled
|
||||||
|
# in — we need a generic mage to pick up veans/magefile.go.
|
||||||
|
run: go install github.com/magefile/mage@v1.17.2
|
||||||
|
- name: install upx
|
||||||
|
run: |
|
||||||
|
wget https://github.com/upx/upx/releases/download/v5.0.0/upx-5.0.0-amd64_linux.tar.xz
|
||||||
|
echo 'b32abf118d721358a50f1aa60eacdbf3298df379c431c3a86f139173ab8289a1 upx-5.0.0-amd64_linux.tar.xz' > upx-5.0.0-amd64_linux.tar.xz.sha256
|
||||||
|
sha256sum -c upx-5.0.0-amd64_linux.tar.xz.sha256
|
||||||
|
tar xf upx-5.0.0-amd64_linux.tar.xz
|
||||||
|
mv upx-5.0.0-amd64_linux/upx /usr/local/bin
|
||||||
|
- name: setup xgo cache
|
||||||
|
uses: useblacksmith/cache@71c7c918062ba3861252d84b07fe5ab2a6b467a6 # v5
|
||||||
|
with:
|
||||||
|
path: /home/runner/.xgo-cache
|
||||||
|
key: veans-${{ hashFiles('veans/go.sum') }}
|
||||||
|
restore-keys: |
|
||||||
|
veans-${{ runner.os }}-go-
|
||||||
|
- name: build and release
|
||||||
|
working-directory: veans
|
||||||
|
env:
|
||||||
|
RELEASE_VERSION: ${{ steps.ghd.outputs.describe }}
|
||||||
|
XGO_OUT_NAME: veans-${{ github.ref_type == 'tag' && steps.ghd.outputs.describe || 'unstable' }}
|
||||||
|
run: |
|
||||||
|
export PATH=$PATH:$GOPATH/bin
|
||||||
|
mage release
|
||||||
|
- name: GPG setup
|
||||||
|
uses: kolaente/action-gpg@main
|
||||||
|
with:
|
||||||
|
gpg-passphrase: "${{ secrets.RELEASE_GPG_PASSPHRASE }}"
|
||||||
|
gpg-sign-key: "${{ secrets.RELEASE_GPG_SIGN_KEY }}"
|
||||||
|
- name: sign
|
||||||
|
working-directory: veans
|
||||||
|
run: |
|
||||||
|
ls -hal dist/zip/*
|
||||||
|
for file in dist/zip/*; do
|
||||||
|
gpg -v --default-key 7D061A4AA61436B40713D42EFF054DACD908493A -b --batch --yes --passphrase "${{ secrets.RELEASE_GPG_PASSPHRASE }}" --pinentry-mode loopback --sign "$file"
|
||||||
|
done
|
||||||
|
- name: Upload
|
||||||
|
uses: kolaente/s3-action@main
|
||||||
|
with:
|
||||||
|
s3-access-key-id: ${{ secrets.S3_ACCESS_KEY }}
|
||||||
|
s3-secret-access-key: ${{ secrets.S3_SECRET_KEY }}
|
||||||
|
s3-endpoint: ${{ secrets.S3_ENDPOINT }}
|
||||||
|
s3-bucket: ${{ secrets.S3_BUCKET }}
|
||||||
|
s3-region: ${{ secrets.S3_REGION }}
|
||||||
|
target-path: /veans/${{ github.ref_type == 'tag' && steps.ghd.outputs.describe || 'unstable' }}
|
||||||
|
files: "veans/dist/zip/*"
|
||||||
|
strip-path-prefix: veans/dist/zip/
|
||||||
|
- name: Store Binaries
|
||||||
|
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
|
||||||
|
with:
|
||||||
|
name: veans_bins
|
||||||
|
path: ./veans/dist/binaries/*
|
||||||
|
- name: Store Binary Packages
|
||||||
|
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
|
||||||
|
if: ${{ github.ref_type == 'tag' }}
|
||||||
|
with:
|
||||||
|
name: veans_bin_packages
|
||||||
|
path: ./veans/dist/zip/*
|
||||||
|
|
||||||
os-package:
|
os-package:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs:
|
needs:
|
||||||
|
|
@ -231,10 +302,110 @@ jobs:
|
||||||
name: vikunja_os_package_${{ matrix.package }}_${{ matrix.arch.pkg }}
|
name: vikunja_os_package_${{ matrix.package }}_${{ matrix.arch.pkg }}
|
||||||
path: ./dist/os-packages/*
|
path: ./dist/os-packages/*
|
||||||
|
|
||||||
|
veans-os-package:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs:
|
||||||
|
- veans-binaries
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
package:
|
||||||
|
- rpm
|
||||||
|
- deb
|
||||||
|
- apk
|
||||||
|
- archlinux
|
||||||
|
arch:
|
||||||
|
- go_name: linux-amd64
|
||||||
|
nfpm: amd64
|
||||||
|
pkg: x86_64
|
||||||
|
- go_name: linux-arm64
|
||||||
|
nfpm: arm64
|
||||||
|
pkg: aarch64
|
||||||
|
- go_name: linux-arm-7
|
||||||
|
nfpm: arm7
|
||||||
|
pkg: armv7
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
|
||||||
|
- name: Download Veans Binaries
|
||||||
|
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7
|
||||||
|
with:
|
||||||
|
name: veans_bins
|
||||||
|
path: ./veans-binaries
|
||||||
|
- name: Git describe
|
||||||
|
id: ghd
|
||||||
|
uses: proudust/gh-describe@v2
|
||||||
|
- uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v6
|
||||||
|
with:
|
||||||
|
go-version: stable
|
||||||
|
- name: Install mage
|
||||||
|
# Generic mage to pick up veans/magefile.go (the cached mage-static
|
||||||
|
# has the parent magefile compiled in).
|
||||||
|
run: go install github.com/magefile/mage@v1.17.2
|
||||||
|
- name: Write GPG key for nfpm
|
||||||
|
if: matrix.package == 'rpm'
|
||||||
|
run: echo -n "${{ secrets.RELEASE_GPG_SIGN_KEY }}" > /tmp/nfpm-signing-key.gpg
|
||||||
|
- name: GPG setup for package signing
|
||||||
|
if: matrix.package == 'archlinux'
|
||||||
|
uses: kolaente/action-gpg@main
|
||||||
|
with:
|
||||||
|
gpg-passphrase: "${{ secrets.RELEASE_GPG_PASSPHRASE }}"
|
||||||
|
gpg-sign-key: "${{ secrets.RELEASE_GPG_SIGN_KEY }}"
|
||||||
|
- name: Prepare
|
||||||
|
env:
|
||||||
|
RELEASE_VERSION: ${{ steps.ghd.outputs.describe }}
|
||||||
|
NFPM_ARCH: ${{ matrix.arch.nfpm }}
|
||||||
|
# The nfpm action runs from $GITHUB_WORKSPACE while the source dir
|
||||||
|
# is also called ./veans — stage the binary under a distinct name
|
||||||
|
# so the two don't collide.
|
||||||
|
NFPM_BIN_PATH: ./veans/veans-bin
|
||||||
|
working-directory: veans
|
||||||
|
run: |
|
||||||
|
export PATH=$PATH:$GOPATH/bin
|
||||||
|
mage release:prepare-nfpm-config
|
||||||
|
mkdir -p ./dist/os-packages
|
||||||
|
mv ../veans-binaries/veans-*-${{ matrix.arch.go_name }} ./veans-bin
|
||||||
|
chmod +x ./veans-bin
|
||||||
|
- name: Create package
|
||||||
|
id: nfpm
|
||||||
|
uses: kolaente/action-gh-nfpm@master
|
||||||
|
with:
|
||||||
|
packager: ${{ matrix.package }}
|
||||||
|
target: ./veans/dist/os-packages/veans-${{ github.ref_type == 'tag' && steps.ghd.outputs.describe || 'unstable' }}-${{ matrix.arch.pkg }}.${{ matrix.package }}
|
||||||
|
config: ./veans/nfpm.yaml
|
||||||
|
env:
|
||||||
|
NFPM_GPG_KEY_FILE: ${{ (matrix.package == 'rpm') && '/tmp/nfpm-signing-key.gpg' || '' }}
|
||||||
|
NFPM_PASSPHRASE: ${{ (matrix.package == 'rpm') && secrets.RELEASE_GPG_PASSPHRASE || '' }}
|
||||||
|
- name: Sign package
|
||||||
|
if: matrix.package == 'archlinux'
|
||||||
|
run: |
|
||||||
|
gpg --default-key 7D061A4AA61436B40713D42EFF054DACD908493A \
|
||||||
|
--batch --yes \
|
||||||
|
--passphrase "${{ secrets.RELEASE_GPG_PASSPHRASE }}" \
|
||||||
|
--pinentry-mode loopback \
|
||||||
|
--detach-sign \
|
||||||
|
./veans/dist/os-packages/veans-${{ github.ref_type == 'tag' && steps.ghd.outputs.describe || 'unstable' }}-${{ matrix.arch.pkg }}.${{ matrix.package }}
|
||||||
|
- name: Upload
|
||||||
|
uses: kolaente/s3-action@main
|
||||||
|
with:
|
||||||
|
s3-access-key-id: ${{ secrets.S3_ACCESS_KEY }}
|
||||||
|
s3-secret-access-key: ${{ secrets.S3_SECRET_KEY }}
|
||||||
|
s3-endpoint: ${{ secrets.S3_ENDPOINT }}
|
||||||
|
s3-bucket: ${{ secrets.S3_BUCKET }}
|
||||||
|
s3-region: ${{ secrets.S3_REGION }}
|
||||||
|
target-path: /veans/${{ github.ref_type == 'tag' && steps.ghd.outputs.describe || 'unstable' }}
|
||||||
|
files: "veans/dist/os-packages/*"
|
||||||
|
strip-path-prefix: veans/dist/os-packages/
|
||||||
|
- name: Store OS Packages
|
||||||
|
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
|
||||||
|
with:
|
||||||
|
name: veans_os_package_${{ matrix.package }}_${{ matrix.arch.pkg }}
|
||||||
|
path: ./veans/dist/os-packages/*
|
||||||
|
|
||||||
publish-repos:
|
publish-repos:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs:
|
needs:
|
||||||
- os-package
|
- os-package
|
||||||
|
- veans-os-package
|
||||||
- desktop
|
- desktop
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
|
|
@ -272,6 +443,16 @@ jobs:
|
||||||
merge-multiple: true
|
merge-multiple: true
|
||||||
path: dist/repo-work/incoming
|
path: dist/repo-work/incoming
|
||||||
|
|
||||||
|
- name: Download all veans OS packages
|
||||||
|
# Merged into the same incoming dir so reprepro / createrepo_c /
|
||||||
|
# repo-add / the apk loop pick them up alongside vikunja's packages
|
||||||
|
# — same suite, same arch fan-out, no extra source entry for users.
|
||||||
|
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7
|
||||||
|
with:
|
||||||
|
pattern: veans_os_package_*
|
||||||
|
merge-multiple: true
|
||||||
|
path: dist/repo-work/incoming
|
||||||
|
|
||||||
- name: Download desktop packages (Linux)
|
- name: Download desktop packages (Linux)
|
||||||
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7
|
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7
|
||||||
with:
|
with:
|
||||||
|
|
@ -538,6 +719,8 @@ jobs:
|
||||||
needs:
|
needs:
|
||||||
- binaries
|
- binaries
|
||||||
- os-package
|
- os-package
|
||||||
|
- veans-binaries
|
||||||
|
- veans-os-package
|
||||||
- desktop
|
- desktop
|
||||||
- publish-repos
|
- publish-repos
|
||||||
if: ${{ github.ref_type == 'tag' }}
|
if: ${{ github.ref_type == 'tag' }}
|
||||||
|
|
@ -555,6 +738,17 @@ jobs:
|
||||||
pattern: vikunja_os_package_*
|
pattern: vikunja_os_package_*
|
||||||
merge-multiple: true
|
merge-multiple: true
|
||||||
|
|
||||||
|
- name: Download Veans Binaries
|
||||||
|
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7
|
||||||
|
with:
|
||||||
|
name: veans_bin_packages
|
||||||
|
|
||||||
|
- name: Download Veans OS Packages
|
||||||
|
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7
|
||||||
|
with:
|
||||||
|
pattern: veans_os_package_*
|
||||||
|
merge-multiple: true
|
||||||
|
|
||||||
- name: Download Desktop Package Linux
|
- name: Download Desktop Package Linux
|
||||||
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7
|
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7
|
||||||
with:
|
with:
|
||||||
|
|
@ -581,4 +775,9 @@ jobs:
|
||||||
vikunja*.deb
|
vikunja*.deb
|
||||||
vikunja*.apk
|
vikunja*.apk
|
||||||
vikunja*.archlinux
|
vikunja*.archlinux
|
||||||
|
veans*.zip
|
||||||
|
veans*.rpm
|
||||||
|
veans*.deb
|
||||||
|
veans*.apk
|
||||||
|
veans*.archlinux
|
||||||
Vikunja Desktop*
|
Vikunja Desktop*
|
||||||
|
|
|
||||||
|
|
@ -1,2 +1,3 @@
|
||||||
/veans
|
/veans
|
||||||
/veans.exe
|
/veans.exe
|
||||||
|
/dist/
|
||||||
|
|
|
||||||
|
|
@ -21,11 +21,16 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/sha256"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"runtime"
|
"runtime"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
|
||||||
"github.com/magefile/mage/mg"
|
"github.com/magefile/mage/mg"
|
||||||
"github.com/magefile/mage/sh"
|
"github.com/magefile/mage/sh"
|
||||||
|
|
@ -42,9 +47,9 @@ func Build() error {
|
||||||
|
|
||||||
// Clean removes built artifacts.
|
// Clean removes built artifacts.
|
||||||
func Clean() error {
|
func Clean() error {
|
||||||
for _, p := range []string{"./veans", "./veans.exe"} {
|
for _, p := range []string{"./veans", "./veans.exe", "./" + releaseDist} {
|
||||||
if _, err := os.Stat(p); err == nil {
|
if _, err := os.Stat(p); err == nil {
|
||||||
if err := os.Remove(p); err != nil {
|
if err := os.RemoveAll(p); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -122,6 +127,457 @@ func (Lint) Fix() error {
|
||||||
return sh.RunV("golangci-lint", "run", "--fix", "./...")
|
return sh.RunV("golangci-lint", "run", "--fix", "./...")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
// Release
|
||||||
|
//
|
||||||
|
// Cross-compiles the veans binary for every OS/arch the parent vikunja binary
|
||||||
|
// targets, runs upx where supported, bundles each into a zip with the LICENSE
|
||||||
|
// and a sha256, and templates nfpm.yaml so the CI can build deb/rpm/apk/
|
||||||
|
// archlinux packages from the same artifacts. Everything lands under
|
||||||
|
// `<veans>/dist/`. The CI workflow uploads dist/zip/* to S3 /veans/<ver>/ and
|
||||||
|
// hands dist/binaries/* off to the nfpm job.
|
||||||
|
|
||||||
|
const releaseDist = "dist"
|
||||||
|
|
||||||
|
type Release mg.Namespace
|
||||||
|
|
||||||
|
var (
|
||||||
|
releaseVersionNumber string
|
||||||
|
releaseVersionString string
|
||||||
|
releaseLdflags string
|
||||||
|
releaseTags = "netgo osusergo"
|
||||||
|
releaseInitOnce sync.Once
|
||||||
|
releaseInitErr error
|
||||||
|
)
|
||||||
|
|
||||||
|
func releaseInitVars(ctx context.Context) error {
|
||||||
|
releaseInitOnce.Do(func() {
|
||||||
|
num := os.Getenv("RELEASE_VERSION")
|
||||||
|
if num == "" {
|
||||||
|
out, err := exec.CommandContext(ctx, "git", "describe", "--tags", "--always", "--abbrev=10").Output()
|
||||||
|
if err != nil {
|
||||||
|
releaseInitErr = fmt.Errorf("git describe: %w", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
num = strings.TrimSpace(string(out))
|
||||||
|
}
|
||||||
|
releaseVersionNumber = strings.Replace(strings.Trim(num, "\n"), "-g", "-", 1)
|
||||||
|
switch releaseVersionNumber {
|
||||||
|
case "", "main":
|
||||||
|
releaseVersionString = "unstable"
|
||||||
|
default:
|
||||||
|
releaseVersionString = releaseVersionNumber
|
||||||
|
}
|
||||||
|
releaseLdflags = fmt.Sprintf(`-X main.version=%s`, releaseVersionNumber)
|
||||||
|
})
|
||||||
|
return releaseInitErr
|
||||||
|
}
|
||||||
|
|
||||||
|
// Release runs all release steps end-to-end: dirs → xgo (windows/linux/darwin
|
||||||
|
// in parallel) → upx → copy → sha256 → per-target bundle dirs → zip.
|
||||||
|
func (Release) Release(ctx context.Context) error {
|
||||||
|
mg.Deps(releaseInitVars)
|
||||||
|
if err := (Release{}).Dirs(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := releasePrepareXgo(ctx); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run cross-compilation per OS in parallel; xgo serializes targets
|
||||||
|
// inside the docker container so each OS still gets full CPU.
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
var (
|
||||||
|
mu sync.Mutex
|
||||||
|
firstErr error
|
||||||
|
)
|
||||||
|
record := func(err error) {
|
||||||
|
if err == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
mu.Lock()
|
||||||
|
if firstErr == nil {
|
||||||
|
firstErr = err
|
||||||
|
}
|
||||||
|
mu.Unlock()
|
||||||
|
}
|
||||||
|
for _, fn := range []func(context.Context) error{
|
||||||
|
(Release{}).Windows,
|
||||||
|
(Release{}).Linux,
|
||||||
|
(Release{}).Darwin,
|
||||||
|
} {
|
||||||
|
wg.Add(1)
|
||||||
|
go func(f func(context.Context) error) {
|
||||||
|
defer wg.Done()
|
||||||
|
record(f(ctx))
|
||||||
|
}(fn)
|
||||||
|
}
|
||||||
|
wg.Wait()
|
||||||
|
if firstErr != nil {
|
||||||
|
return firstErr
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := (Release{}).Compress(ctx); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := (Release{}).Copy(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := (Release{}).Check(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := (Release{}).OsPackage(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return (Release{}).Zip(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dirs creates all directories needed to release veans.
|
||||||
|
func (Release) Dirs() error {
|
||||||
|
for _, d := range []string{"binaries", "release", "zip"} {
|
||||||
|
if err := os.MkdirAll(filepath.Join(releaseDist, d), 0o755); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func releasePrepareXgo(_ context.Context) error {
|
||||||
|
if _, err := exec.LookPath("xgo"); err != nil {
|
||||||
|
fmt.Println("xgo not found, installing src.techknowlogick.com/xgo...")
|
||||||
|
if err := sh.RunV("go", "install", "src.techknowlogick.com/xgo@latest"); err != nil {
|
||||||
|
return fmt.Errorf("installing xgo: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fmt.Println("Pulling latest xgo docker image...")
|
||||||
|
return sh.RunV("docker", "pull", "ghcr.io/techknowlogick/xgo:latest")
|
||||||
|
}
|
||||||
|
|
||||||
|
func runXgo(ctx context.Context, targets string) error {
|
||||||
|
mg.Deps(releaseInitVars)
|
||||||
|
if err := releasePrepareXgo(ctx); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
extraLdflags := `-linkmode external -extldflags "-static" `
|
||||||
|
// xgo's darwin builds can't use the static external linker.
|
||||||
|
if strings.HasPrefix(targets, "darwin") {
|
||||||
|
extraLdflags = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
outName := os.Getenv("XGO_OUT_NAME")
|
||||||
|
if outName == "" {
|
||||||
|
outName = "veans-" + releaseVersionString
|
||||||
|
}
|
||||||
|
|
||||||
|
return sh.RunV("xgo",
|
||||||
|
"-dest", filepath.Join(releaseDist, "binaries"),
|
||||||
|
"-tags", releaseTags,
|
||||||
|
"-ldflags", extraLdflags+releaseLdflags,
|
||||||
|
"-targets", targets,
|
||||||
|
"-out", outName,
|
||||||
|
"./cmd/veans",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Windows builds binaries for windows. Same target set as parent vikunja.
|
||||||
|
func (Release) Windows(ctx context.Context) error {
|
||||||
|
return runXgo(ctx, "windows/*")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Linux builds binaries for linux. Same target set as parent vikunja.
|
||||||
|
func (Release) Linux(ctx context.Context) error {
|
||||||
|
targets := []string{
|
||||||
|
"linux/amd64",
|
||||||
|
"linux/arm-5",
|
||||||
|
"linux/arm-6",
|
||||||
|
"linux/arm-7",
|
||||||
|
"linux/arm64",
|
||||||
|
"linux/mips",
|
||||||
|
"linux/mipsle",
|
||||||
|
"linux/mips64",
|
||||||
|
"linux/mips64le",
|
||||||
|
"linux/riscv64",
|
||||||
|
}
|
||||||
|
return runXgo(ctx, strings.Join(targets, ","))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Darwin builds binaries for macOS. Same minimum (10.15) as parent.
|
||||||
|
func (Release) Darwin(ctx context.Context) error {
|
||||||
|
return runXgo(ctx, "darwin-10.15/*")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Xgo cross-compiles a single os/arch[-variant] target.
|
||||||
|
func (Release) Xgo(ctx context.Context, target string) error {
|
||||||
|
parts := strings.Split(target, "/")
|
||||||
|
if len(parts) < 2 {
|
||||||
|
return fmt.Errorf("invalid target %q (expected os/arch[/variant])", target)
|
||||||
|
}
|
||||||
|
variant := ""
|
||||||
|
if len(parts) > 2 && parts[2] != "" {
|
||||||
|
variant = "-" + strings.ReplaceAll(parts[2], "v", "")
|
||||||
|
}
|
||||||
|
return runXgo(ctx, parts[0]+"/"+parts[1]+variant)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compress runs upx -9 over each built binary that upx can actually handle.
|
||||||
|
// Skip list matches the parent vikunja magefile.
|
||||||
|
func (Release) Compress(_ context.Context) error {
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
var (
|
||||||
|
mu sync.Mutex
|
||||||
|
firstErr error
|
||||||
|
)
|
||||||
|
record := func(err error) {
|
||||||
|
if err == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
mu.Lock()
|
||||||
|
if firstErr == nil {
|
||||||
|
firstErr = err
|
||||||
|
}
|
||||||
|
mu.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
walkErr := filepath.Walk(filepath.Join(releaseDist, "binaries"), func(path string, info os.FileInfo, err error) error {
|
||||||
|
if err != nil || info.IsDir() {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
name := info.Name()
|
||||||
|
if !strings.Contains(name, "veans") {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if strings.Contains(name, "mips") ||
|
||||||
|
strings.Contains(name, "s390x") ||
|
||||||
|
strings.Contains(name, "riscv64") ||
|
||||||
|
strings.Contains(name, "darwin") ||
|
||||||
|
(strings.Contains(name, "windows") && strings.Contains(name, "arm64")) {
|
||||||
|
// upx can't compress these targets.
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
wg.Add(1)
|
||||||
|
go func(p string) {
|
||||||
|
defer wg.Done()
|
||||||
|
if err := sh.RunV("chmod", "+x", p); err != nil {
|
||||||
|
record(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
record(sh.RunV("upx", "-9", p))
|
||||||
|
}(path)
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if walkErr != nil {
|
||||||
|
return walkErr
|
||||||
|
}
|
||||||
|
wg.Wait()
|
||||||
|
return firstErr
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy copies all built binaries to dist/release/ as the staging area for
|
||||||
|
// per-target bundles and nfpm.
|
||||||
|
func (Release) Copy() error {
|
||||||
|
return filepath.Walk(filepath.Join(releaseDist, "binaries"), func(path string, info os.FileInfo, err error) error {
|
||||||
|
if err != nil || info.IsDir() {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !strings.Contains(info.Name(), "veans") {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return copyFile(path, filepath.Join(releaseDist, "release", info.Name()))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check writes a sha256 file next to each binary in dist/release/.
|
||||||
|
func (Release) Check() error {
|
||||||
|
p := filepath.Join(releaseDist, "release")
|
||||||
|
return filepath.Walk(p, func(path string, info os.FileInfo, err error) error {
|
||||||
|
if err != nil || info.IsDir() {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if strings.HasSuffix(info.Name(), ".sha256") {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
sum, err := sha256File(path)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return os.WriteFile(path+".sha256", []byte(sum+" "+info.Name()+"\n"), 0o644)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// OsPackage creates one folder per binary in dist/release/, populated with
|
||||||
|
// the binary, its sha256, and the LICENSE so the bundle is self-contained.
|
||||||
|
func (Release) OsPackage() error {
|
||||||
|
p := filepath.Join(releaseDist, "release")
|
||||||
|
|
||||||
|
// Snapshot first so we don't walk into the newly-created folders.
|
||||||
|
bins := map[string]os.FileInfo{}
|
||||||
|
if err := filepath.Walk(p, func(path string, info os.FileInfo, err error) error {
|
||||||
|
if err != nil || info.IsDir() {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if strings.HasSuffix(info.Name(), ".sha256") {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
bins[path] = info
|
||||||
|
return nil
|
||||||
|
}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
licensePath, err := licenseSource()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for binPath, info := range bins {
|
||||||
|
folder := filepath.Join(p, info.Name()+"-full") + string(os.PathSeparator)
|
||||||
|
if err := os.MkdirAll(folder, 0o755); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := moveFile(binPath+".sha256", filepath.Join(folder, info.Name()+".sha256")); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := moveFile(binPath, filepath.Join(folder, info.Name())); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := copyFile(licensePath, filepath.Join(folder, "LICENSE")); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Zip turns each per-target folder under dist/release/<name>-full/ into
|
||||||
|
// dist/zip/<name>-full.zip. Uses the system `zip` so we get the same on-wire
|
||||||
|
// format as the parent's release artifacts.
|
||||||
|
func (Release) Zip(ctx context.Context) error {
|
||||||
|
rootDir, err := os.Getwd()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
p := filepath.Join(releaseDist, "release")
|
||||||
|
return filepath.Walk(p, func(path string, info os.FileInfo, err error) error {
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !info.IsDir() || info.Name() == "release" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
fmt.Printf("Zipping %s...\n", info.Name())
|
||||||
|
zipFile := filepath.Join(rootDir, releaseDist, "zip", info.Name()+".zip")
|
||||||
|
//nolint:gosec // mage build helper; arguments are derived from the local fs walk above.
|
||||||
|
c := exec.CommandContext(ctx, "zip", "-r", zipFile, ".", "-i", "*")
|
||||||
|
c.Dir = path
|
||||||
|
c.Stdout = os.Stdout
|
||||||
|
c.Stderr = os.Stderr
|
||||||
|
return c.Run()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// PrepareNFPMConfig templates ./nfpm.yaml in place, substituting <version>,
|
||||||
|
// <arch> and <binlocation> the same way the parent magefile does. Set
|
||||||
|
// NFPM_ARCH to the nfpm arch name (amd64, arm64, arm7, 386) before calling.
|
||||||
|
// The substituted file is meant to be consumed by `nfpm pkg` immediately
|
||||||
|
// after; this is destructive and intentional (the CI checks the repo out
|
||||||
|
// fresh per job).
|
||||||
|
func (Release) PrepareNFPMConfig() error {
|
||||||
|
mg.Deps(releaseInitVars)
|
||||||
|
cfgPath := "./nfpm.yaml"
|
||||||
|
raw, err := os.ReadFile(cfgPath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var nfpmArch string
|
||||||
|
switch os.Getenv("NFPM_ARCH") {
|
||||||
|
case "arm64":
|
||||||
|
nfpmArch = "arm64"
|
||||||
|
case "arm7":
|
||||||
|
nfpmArch = "arm7"
|
||||||
|
case "386":
|
||||||
|
nfpmArch = "386"
|
||||||
|
default:
|
||||||
|
nfpmArch = "amd64"
|
||||||
|
}
|
||||||
|
|
||||||
|
// nfpm resolves <binlocation> relative to its working directory. In CI the
|
||||||
|
// nfpm action runs from $GITHUB_WORKSPACE while the veans source already
|
||||||
|
// occupies ./veans, so the CI stages the binary at ./veans/veans-bin and
|
||||||
|
// passes NFPM_BIN_PATH=./veans/veans-bin. Outside CI the default works for
|
||||||
|
// a local `mage build && mage release:prepare-nfpm-config && nfpm pkg
|
||||||
|
// --config nfpm.yaml` from inside veans/.
|
||||||
|
binLocation := os.Getenv("NFPM_BIN_PATH")
|
||||||
|
if binLocation == "" {
|
||||||
|
binLocation = "./veans"
|
||||||
|
}
|
||||||
|
|
||||||
|
fixed := strings.ReplaceAll(string(raw), "<version>", releaseVersionNumber)
|
||||||
|
fixed = strings.ReplaceAll(fixed, "<arch>", nfpmArch)
|
||||||
|
fixed = strings.ReplaceAll(fixed, "<binlocation>", binLocation)
|
||||||
|
return os.WriteFile(cfgPath, []byte(fixed), 0o600)
|
||||||
|
}
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
// helpers
|
||||||
|
|
||||||
|
func copyFile(src, dst string) error {
|
||||||
|
in, err := os.Open(src)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer in.Close()
|
||||||
|
out, err := os.Create(dst)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer out.Close()
|
||||||
|
if _, err := io.Copy(out, in); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
si, err := os.Stat(src)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := os.Chmod(dst, si.Mode()); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return out.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func moveFile(src, dst string) error {
|
||||||
|
if err := copyFile(src, dst); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return os.Remove(src)
|
||||||
|
}
|
||||||
|
|
||||||
|
func sha256File(path string) (string, error) {
|
||||||
|
f, err := os.Open(path)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
h := sha256.New()
|
||||||
|
if _, err := io.Copy(h, f); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%x", h.Sum(nil)), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// licenseSource resolves the AGPLv3 LICENSE file. veans intentionally doesn't
|
||||||
|
// vendor its own copy — the parent repo's LICENSE applies to both. Look in
|
||||||
|
// ../LICENSE (the normal layout when running from veans/) and fall back to
|
||||||
|
// ./LICENSE for unusual checkouts.
|
||||||
|
func licenseSource() (string, error) {
|
||||||
|
for _, p := range []string{"../LICENSE", "./LICENSE"} {
|
||||||
|
if _, err := os.Stat(p); err == nil {
|
||||||
|
return p, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "", fmt.Errorf("could not find LICENSE in ../ or ./")
|
||||||
|
}
|
||||||
|
|
||||||
// Aliases lets `mage test` resolve to `Test.All` (and the others) without
|
// Aliases lets `mage test` resolve to `Test.All` (and the others) without
|
||||||
// having to spell out the namespace. Mirrors the parent magefile's pattern.
|
// having to spell out the namespace. Mirrors the parent magefile's pattern.
|
||||||
var Aliases = map[string]any{
|
var Aliases = map[string]any{
|
||||||
|
|
@ -130,4 +586,7 @@ var Aliases = map[string]any{
|
||||||
"test:e2e": Test.E2E,
|
"test:e2e": Test.E2E,
|
||||||
"lint": Lint.All,
|
"lint": Lint.All,
|
||||||
"lint:fix": Lint.Fix,
|
"lint:fix": Lint.Fix,
|
||||||
|
"release": Release.Release,
|
||||||
|
"release:xgo": Release.Xgo,
|
||||||
|
"release:prepare-nfpm-config": Release.PrepareNFPMConfig,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
name: "veans"
|
||||||
|
arch: "<arch>"
|
||||||
|
platform: "linux"
|
||||||
|
version: "<version>"
|
||||||
|
description: "veans is an agent-friendly CLI wrapper for the Vikunja REST API."
|
||||||
|
maintainer: "Vikunja Maintainers <maintainers@vikunja.io>"
|
||||||
|
homepage: "https://vikunja.io"
|
||||||
|
section: "default"
|
||||||
|
priority: "extra"
|
||||||
|
license: "AGPLv3"
|
||||||
|
rpm:
|
||||||
|
signature:
|
||||||
|
key_file: ${NFPM_GPG_KEY_FILE}
|
||||||
|
contents:
|
||||||
|
- src: <binlocation>
|
||||||
|
dst: /usr/local/bin/veans
|
||||||
Loading…
Reference in New Issue