feat: add cross-platform release packaging

This commit is contained in:
mirivlad 2026-06-06 10:49:56 +08:00
parent 48c0057d11
commit 3e82183d5c
14 changed files with 348 additions and 28 deletions

View File

@ -1,6 +1,7 @@
APP=sshkeeper APP=sshkeeper
RELEASE_CHECK_DIR ?= /tmp/sshkeeper-release-check
.PHONY: build run test vet fmt clean install .PHONY: build run test vet fmt clean install release-check
build: build:
go build -o bin/$(APP) . go build -o bin/$(APP) .
@ -22,3 +23,15 @@ clean:
install: install:
go build -o $(HOME)/.local/bin/$(APP) . go build -o $(HOME)/.local/bin/$(APP) .
release-check:
rm -rf $(RELEASE_CHECK_DIR)
mkdir -p $(RELEASE_CHECK_DIR)
go test ./...
go vet ./...
CGO_ENABLED=0 go build -o $(RELEASE_CHECK_DIR)/$(APP) .
GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -o $(RELEASE_CHECK_DIR)/$(APP)-linux-amd64 .
GOOS=linux GOARCH=arm64 CGO_ENABLED=0 go build -o $(RELEASE_CHECK_DIR)/$(APP)-linux-arm64 .
GOOS=darwin GOARCH=amd64 CGO_ENABLED=0 go build -o $(RELEASE_CHECK_DIR)/$(APP)-darwin-amd64 .
GOOS=darwin GOARCH=arm64 CGO_ENABLED=0 go build -o $(RELEASE_CHECK_DIR)/$(APP)-darwin-arm64 .
GOOS=windows GOARCH=amd64 CGO_ENABLED=0 go build -o $(RELEASE_CHECK_DIR)/$(APP)-windows-amd64.exe .

View File

@ -1,7 +1,8 @@
# sshkeeper # sshkeeper
`sshkeeper` is a Linux console manager for SSH profiles, secrets, and quick `sshkeeper` is a console manager for SSH profiles, secrets, and quick OpenSSH
OpenSSH launches. It does not replace OpenSSH; it keeps connection metadata in a launches. Primary target: Linux. macOS builds are available, and Windows builds
are experimental. It does not replace OpenSSH; it keeps connection metadata in a
local SQLite database, keeps passwords/passphrases in an encrypted vault, and local SQLite database, keeps passwords/passphrases in an encrypted vault, and
starts the system `ssh` client with the right options. starts the system `ssh` client with the right options.
@ -42,10 +43,24 @@ Or use the build scripts:
```bash ```bash
./build.sh # Build binary to bin/ ./build.sh # Build binary to bin/
./release.sh # Build release tarballs to dist/ ./release.sh # Build release archives to dist/
``` ```
Requirements: Go 1.25+, Linux x86_64, system OpenSSH. Requirements: Go 1.25+ and system OpenSSH.
Platform status:
| Platform | Status | Notes |
|----------|--------|-------|
| Linux | Primary target | `linux/amd64` and `linux/arm64` release tarballs are available. |
| macOS | Supported, needs verification on target machines | `darwin/amd64` and `darwin/arm64` release tarballs are available. Requires system `ssh` client. Homebrew formula planned. |
| Windows | Experimental | Requires OpenSSH Client available as `ssh.exe` in `PATH`. Password/key-passphrase PTY flows are not validated on Windows. |
On Windows, install OpenSSH Client via Windows Optional Features or PowerShell:
```powershell
Add-WindowsCapability -Online -Name OpenSSH.Client~~~~0.0.1.0
```
**Source repositories:** **Source repositories:**
- Main public: [github.com/mirivlad/sshkeeper](https://github.com/mirivlad/sshkeeper) - Main public: [github.com/mirivlad/sshkeeper](https://github.com/mirivlad/sshkeeper)
@ -55,8 +70,7 @@ Requirements: Go 1.25+, Linux x86_64, system OpenSSH.
```bash ```bash
tar -xzf sshkeeper_v0.2.0_linux_amd64.tar.gz tar -xzf sshkeeper_v0.2.0_linux_amd64.tar.gz
chmod +x sshkeeper-linux-amd64 sudo install -m 0755 sshkeeper_v0.2.0_linux_amd64/sshkeeper /usr/local/bin/sshkeeper
sudo install -m 0755 sshkeeper-linux-amd64 /usr/local/bin/sshkeeper
sshkeeper sshkeeper
``` ```
@ -285,10 +299,13 @@ If `XDG_CONFIG_HOME` or `XDG_DATA_HOME` are set, sshkeeper stores data under
```bash ```bash
go test ./... go test ./...
go build -o bin/sshkeeper . go build -o bin/sshkeeper .
make release-check
``` ```
`bin/` is ignored by git. `bin/` is ignored by git.
For release packaging details, see [docs/release.md](docs/release.md).
## Project Layout ## Project Layout
```text ```text
@ -302,8 +319,9 @@ sshkeeper/
├── internal/vault/ # Encrypted vault ├── internal/vault/ # Encrypted vault
├── internal/tunnel/ # Tunnel state management ├── internal/tunnel/ # Tunnel state management
├── docs/guide.md # User guide ├── docs/guide.md # User guide
├── docs/release.md # Release packaging guide
├── build.sh # Build binary to bin/ ├── build.sh # Build binary to bin/
├── release.sh # Build release tarballs to dist/ ├── release.sh # Build release archives to dist/
└── main.go └── main.go
``` ```

View File

@ -21,7 +21,8 @@ var (
var rootCmd = &cobra.Command{ var rootCmd = &cobra.Command{
Use: "sshkeeper", Use: "sshkeeper",
Short: "sshkeeper — SSH connection manager", Short: "sshkeeper — SSH connection manager",
Long: `sshkeeper is a console SSH connection manager for Linux. Long: `sshkeeper is a console SSH connection manager.
Linux is the primary target; macOS builds are available and Windows is experimental.
It manages server profiles, secrets, and provides a convenient way It manages server profiles, secrets, and provides a convenient way
to launch SSH sessions using the system OpenSSH client.`, to launch SSH sessions using the system OpenSSH client.`,
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {

View File

@ -18,7 +18,7 @@
## Что такое sshkeeper ## Что такое sshkeeper
sshkeeper — это консольный менеджер SSH-подключений для Linux. Он хранит профили серверов, секреты (пароли, фразы от ключей) и запускает системный `ssh` с нужными опциями. sshkeeper — это консольный менеджер SSH-подключений. Основная целевая платформа — Linux; сборки для macOS доступны, Windows-сборка пока экспериментальная. Он хранит профили серверов, секреты (пароли, фразы от ключей) и запускает системный `ssh` с нужными опциями.
**Чем sshkeeper НЕ является:** **Чем sshkeeper НЕ является:**
- Это не Ansible — он не настраивает серверы и не пушит файлы - Это не Ansible — он не настраивает серверы и не пушит файлы
@ -63,14 +63,27 @@ go build -o ~/.local/bin/sshkeeper .
./release.sh # сборка релизных архивов в dist/ ./release.sh # сборка релизных архивов в dist/
``` ```
**Требования:** Go 1.25+, Linux x86_64, системный OpenSSH. **Требования:** Go 1.25+ и системный OpenSSH.
Статус платформ:
| Платформа | Статус | Примечание |
|-----------|--------|------------|
| Linux | Основная целевая платформа | Архивы `linux/amd64` и `linux/arm64`. |
| macOS | Сборки доступны, требуется проверка на целевых машинах | Архивы `darwin/amd64` и `darwin/arm64`, нужен системный `ssh`. Homebrew formula запланирована. |
| Windows | Experimental | Нужен OpenSSH Client как `ssh.exe` в `PATH`; password/key-passphrase PTY-сценарии на Windows пока не подтверждены. |
На Windows OpenSSH Client можно установить через Windows Optional Features или PowerShell:
```powershell
Add-WindowsCapability -Online -Name OpenSSH.Client~~~~0.0.1.0
```
### Из релиза (после публикации v0.2.0) ### Из релиза (после публикации v0.2.0)
```bash ```bash
tar -xzf sshkeeper_v0.2.0_linux_amd64.tar.gz tar -xzf sshkeeper_v0.2.0_linux_amd64.tar.gz
chmod +x sshkeeper-linux-amd64 sudo install -m 0755 sshkeeper_v0.2.0_linux_amd64/sshkeeper /usr/local/bin/sshkeeper
sudo install -m 0755 sshkeeper-linux-amd64 /usr/local/bin/sshkeeper
``` ```
--- ---
@ -192,7 +205,8 @@ sshkeeper — Quick Help
sshkeeper — Full Help sshkeeper — Full Help
What is sshkeeper What is sshkeeper
sshkeeper is a Linux console SSH connection manager. sshkeeper is a console SSH connection manager.
Linux is the primary target; macOS builds are available and Windows is experimental.
It stores server profiles, secrets, and launches the system ssh client. It stores server profiles, secrets, and launches the system ssh client.
Navigation Navigation
@ -782,6 +796,9 @@ go test ./...
# Сборка # Сборка
go build -o bin/sshkeeper . go build -o bin/sshkeeper .
# Проверка релизной кросс-сборки
make release-check
# Или через скрипты # Или через скрипты
./build.sh # сборка в bin/ ./build.sh # сборка в bin/
./release.sh # релизные архивы в dist/ ./release.sh # релизные архивы в dist/

114
docs/release.md Normal file
View File

@ -0,0 +1,114 @@
# Release Packaging
This document describes the manual release flow for sshkeeper.
## Create a Tag
Use a semantic version tag:
```bash
git status --short
git tag -a v0.2.0 -m "sshkeeper v0.2.0"
git push origin v0.2.0
```
The release script uses `git describe --tags --always --dirty` by default. You
can also pass the version explicitly:
```bash
./release.sh v0.2.0
```
or:
```bash
VERSION=v0.2.0 ./release.sh
```
For reproducible archives, the script uses `SOURCE_DATE_EPOCH`. By default it
uses the timestamp of the latest git commit. To force a specific timestamp:
```bash
SOURCE_DATE_EPOCH=1760000000 ./release.sh v0.2.0
```
## Run Release Checks
Before packaging, run:
```bash
make release-check
```
This runs:
- `go test ./...`
- `go vet ./...`
- native `go build`
- `linux/amd64` cross-build with `CGO_ENABLED=0`
- `linux/arm64` cross-build with `CGO_ENABLED=0`
- `darwin/amd64` cross-build with `CGO_ENABLED=0`
- `darwin/arm64` cross-build with `CGO_ENABLED=0`
- `windows/amd64` cross-build with `CGO_ENABLED=0`
## Build Artifacts
Run:
```bash
./release.sh v0.2.0
```
Expected files in `dist/`:
```text
sshkeeper_v0.2.0_linux_amd64.tar.gz
sshkeeper_v0.2.0_linux_arm64.tar.gz
sshkeeper_v0.2.0_darwin_amd64.tar.gz
sshkeeper_v0.2.0_darwin_arm64.tar.gz
sshkeeper_v0.2.0_windows_amd64.zip
checksums.txt
```
Each archive contains:
- `sshkeeper` or `sshkeeper.exe`
- `README.md`
- `LICENSE`
- `docs/guide.md`
## Verify Checksums
From the `dist/` directory:
```bash
sha256sum -c checksums.txt
```
Expected result: every archive reports `OK`.
## Publish in GitHub Release
Upload these files to the release:
- all five platform archives
- `checksums.txt`
Release notes should mention platform status:
- Linux is the primary target.
- macOS builds are available as tar.gz for amd64 and arm64 and require the
system `ssh` client.
- Windows build is experimental and requires OpenSSH Client available as
`ssh.exe` in `PATH`.
## Packaging TODO
Prepare these package channels after the first archive-based release:
- deb package
- Arch PKGBUILD / AUR
- rpm later
- Homebrew tap
- Scoop manifest
- Winget later

View File

@ -3,6 +3,7 @@ package config
import ( import (
"os" "os"
"path/filepath" "path/filepath"
"runtime"
"github.com/BurntSushi/toml" "github.com/BurntSushi/toml"
) )
@ -34,7 +35,7 @@ type UIConfig struct {
func defaultConfig() *Config { func defaultConfig() *Config {
return &Config{ return &Config{
SSH: SSHConfig{ SSH: SSHConfig{
Binary: "/usr/bin/ssh", Binary: defaultSSHBinary(),
ConnectTimeoutSec: 10, ConnectTimeoutSec: 10,
TestCommand: "echo SSHKEEPER_OK", TestCommand: "echo SSHKEEPER_OK",
}, },
@ -47,6 +48,13 @@ func defaultConfig() *Config {
} }
} }
func defaultSSHBinary() string {
if runtime.GOOS == "windows" {
return "ssh.exe"
}
return "/usr/bin/ssh"
}
func Load() (*Config, error) { func Load() (*Config, error) {
cfg := defaultConfig() cfg := defaultConfig()

View File

@ -4,6 +4,7 @@ import (
"fmt" "fmt"
"os" "os"
"os/exec" "os/exec"
"runtime"
"strings" "strings"
"github.com/mirivlad/sshkeeper/internal/config" "github.com/mirivlad/sshkeeper/internal/config"
@ -12,7 +13,34 @@ import (
type VaultFunc func(serverAlias string, secretType string) (string, error) type VaultFunc func(serverAlias string, secretType string) (string, error)
const windowsOpenSSHInstallHint = "Install OpenSSH Client via Windows Optional Features or PowerShell:\nAdd-WindowsCapability -Online -Name OpenSSH.Client~~~~0.0.1.0"
func EnsureSSHBinary(binary string) error {
return validateSSHBinaryForOS(runtime.GOOS, binary, exec.LookPath)
}
func validateSSHBinaryForOS(goos string, binary string, lookPath func(string) (string, error)) error {
if goos == "windows" {
if _, err := lookPath("ssh.exe"); err != nil {
return fmt.Errorf("ssh.exe not found in PATH. Windows build is experimental and requires OpenSSH Client.\n%s", windowsOpenSSHInstallHint)
}
return nil
}
if strings.TrimSpace(binary) == "" {
binary = "ssh"
}
if _, err := lookPath(binary); err != nil {
return fmt.Errorf("ssh binary not found (%s): %w", binary, err)
}
return nil
}
func Connect(cfg *config.Config, server *model.Server, getVault VaultFunc) error { func Connect(cfg *config.Config, server *model.Server, getVault VaultFunc) error {
if err := EnsureSSHBinary(cfg.SSH.Binary); err != nil {
return err
}
args := BuildSSHArgsSimple(server) args := BuildSSHArgsSimple(server)
if strings.TrimSpace(server.StartupCommand) != "" { if strings.TrimSpace(server.StartupCommand) != "" {
args = append(args, server.StartupCommand) args = append(args, server.StartupCommand)
@ -49,6 +77,10 @@ func Connect(cfg *config.Config, server *model.Server, getVault VaultFunc) error
} }
func RunCommand(cfg *config.Config, server *model.Server, getVault VaultFunc, command string) error { func RunCommand(cfg *config.Config, server *model.Server, getVault VaultFunc, command string) error {
if err := EnsureSSHBinary(cfg.SSH.Binary); err != nil {
return err
}
args := BuildSSHArgsSimple(server) args := BuildSSHArgsSimple(server)
args = append(args, command) args = append(args, command)
@ -78,6 +110,10 @@ func RunCommand(cfg *config.Config, server *model.Server, getVault VaultFunc, co
} }
func RunCommandOutput(cfg *config.Config, server *model.Server, getVault VaultFunc, command string) (string, error) { func RunCommandOutput(cfg *config.Config, server *model.Server, getVault VaultFunc, command string) (string, error) {
if err := EnsureSSHBinary(cfg.SSH.Binary); err != nil {
return "", err
}
args := BuildSSHArgsSimple(server) args := BuildSSHArgsSimple(server)
args = append(args, "-o", fmt.Sprintf("ConnectTimeout=%d", cfg.SSH.ConnectTimeoutSec)) args = append(args, "-o", fmt.Sprintf("ConnectTimeout=%d", cfg.SSH.ConnectTimeoutSec))
@ -116,6 +152,10 @@ func RunCommandOutput(cfg *config.Config, server *model.Server, getVault VaultFu
} }
func Test(cfg *config.Config, server *model.Server, getVault VaultFunc) (bool, string) { func Test(cfg *config.Config, server *model.Server, getVault VaultFunc) (bool, string) {
if err := EnsureSSHBinary(cfg.SSH.Binary); err != nil {
return false, err.Error()
}
args := BuildSSHArgsSimple(server) args := BuildSSHArgsSimple(server)
args = append(args, "-o", fmt.Sprintf("ConnectTimeout=%d", cfg.SSH.ConnectTimeoutSec)) args = append(args, "-o", fmt.Sprintf("ConnectTimeout=%d", cfg.SSH.ConnectTimeoutSec))
@ -179,6 +219,10 @@ func testWithPassword(cfg *config.Config, args []string, password string) (bool,
} }
func ConnectWithArgs(cfg *config.Config, args []string, vaultFunc VaultFunc, server *model.Server) error { func ConnectWithArgs(cfg *config.Config, args []string, vaultFunc VaultFunc, server *model.Server) error {
if err := EnsureSSHBinary(cfg.SSH.Binary); err != nil {
return err
}
switch server.AuthMethod { switch server.AuthMethod {
case model.AuthPassword: case model.AuthPassword:
password, err := vaultFunc(server.Alias, "ssh_password") password, err := vaultFunc(server.Alias, "ssh_password")

View File

@ -104,6 +104,40 @@ func TestConnectRunsStartupCommand(t *testing.T) {
} }
} }
func TestValidateSSHBinaryForWindowsRequiresOpenSSHClient(t *testing.T) {
err := validateSSHBinaryForOS("windows", "ssh.exe", func(name string) (string, error) {
if name != "ssh.exe" {
t.Fatalf("expected lookup for ssh.exe, got %q", name)
}
return "", os.ErrNotExist
})
if err == nil {
t.Fatal("expected missing ssh.exe error")
}
if !strings.Contains(err.Error(), "OpenSSH Client") {
t.Fatalf("expected OpenSSH Client guidance, got %q", err)
}
if !strings.Contains(err.Error(), "Add-WindowsCapability -Online -Name OpenSSH.Client~~~~0.0.1.0") {
t.Fatalf("expected PowerShell install command, got %q", err)
}
}
func TestValidateSSHBinaryForNonWindowsChecksConfiguredBinary(t *testing.T) {
var lookedUp string
err := validateSSHBinaryForOS("linux", "/usr/bin/ssh", func(name string) (string, error) {
lookedUp = name
return "/usr/bin/ssh", nil
})
if err != nil {
t.Fatalf("validate ssh binary: %v", err)
}
if lookedUp != "/usr/bin/ssh" {
t.Fatalf("expected lookup for configured binary, got %q", lookedUp)
}
}
func TestBuildSSHArgs_Simple(t *testing.T) { func TestBuildSSHArgs_Simple(t *testing.T) {
server := &model.Server{Host: "example.org", Port: 22, User: "root"} server := &model.Server{Host: "example.org", Port: 22, User: "root"}
args := BuildSSHArgsSimple(server) args := BuildSSHArgsSimple(server)

View File

@ -1,3 +1,5 @@
//go:build !windows
package ssh package ssh
import ( import (

View File

@ -1,3 +1,5 @@
//go:build !windows
package ssh package ssh
import ( import (

View File

@ -0,0 +1,15 @@
//go:build windows
package ssh
import "fmt"
const windowsPTYUnsupportedMessage = "password and key-passphrase auth are not supported on Windows experimental builds; use key or agent auth with OpenSSH Client"
func ConnectWithPassword(sshBinary string, args []string, password string) error {
return fmt.Errorf("%s", windowsPTYUnsupportedMessage)
}
func connectWithPasswordAndRead(sshBinary string, args []string, password string, timeoutSec int) (bool, string) {
return false, windowsPTYUnsupportedMessage
}

View File

@ -159,7 +159,8 @@ func (m *fullHelpModel) View() string {
rows [][2]string rows [][2]string
}{ }{
{"What is sshkeeper", [][2]string{ {"What is sshkeeper", [][2]string{
{"", "sshkeeper is a Linux console SSH connection manager."}, {"", "sshkeeper is a console SSH connection manager."},
{"", "Linux is the primary target; macOS builds are available and Windows is experimental."},
{"", "It stores server profiles, secrets, and launches the system ssh client."}, {"", "It stores server profiles, secrets, and launches the system ssh client."},
{"", ""}, {"", ""},
}}, }},

View File

@ -93,6 +93,10 @@ func Start(cfg *config.Config, server *model.Server, forwards []*model.Forward,
mu.Lock() mu.Lock()
defer mu.Unlock() defer mu.Unlock()
if err := ssh.EnsureSSHBinary(cfg.SSH.Binary); err != nil {
return nil, err
}
// Filter enabled forwards // Filter enabled forwards
var active []*model.Forward var active []*model.Forward
for _, f := range forwards { for _, f := range forwards {

View File

@ -4,26 +4,73 @@ set -euo pipefail
cd "$(dirname "$0")" cd "$(dirname "$0")"
APP=sshkeeper APP=sshkeeper
VERSION=$(git describe --tags --always --dirty 2>/dev/null || echo "dev") VERSION=${VERSION:-${1:-$(git describe --tags --always --dirty 2>/dev/null || echo "dev")}}
LDFLAGS="-s -w -X main.version=${VERSION}" LDFLAGS="-s -w -X main.version=${VERSION}"
DIST_DIR="dist" DIST_DIR="dist"
SOURCE_DATE_EPOCH=${SOURCE_DATE_EPOCH:-$(git log -1 --format=%ct 2>/dev/null || date +%s)}
echo "==> Building release ${APP} ${VERSION}..." echo "==> Building release ${APP} ${VERSION}..."
echo "==> SOURCE_DATE_EPOCH=${SOURCE_DATE_EPOCH}"
rm -rf "${DIST_DIR}" rm -rf "${DIST_DIR}"
mkdir -p "${DIST_DIR}" mkdir -p "${DIST_DIR}"
# Linux amd64 package_docs() {
echo "==> linux/amd64..." local package_dir="$1"
GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -ldflags "${LDFLAGS}" -o "${DIST_DIR}/${APP}" .
tar -czf "${DIST_DIR}/${APP}_${VERSION}_linux_amd64.tar.gz" -C "${DIST_DIR}" "${APP}"
rm -f "${DIST_DIR}/${APP}"
# Linux arm64 cp README.md LICENSE "${package_dir}/"
echo "==> linux/arm64..." if [[ -f docs/guide.md ]]; then
GOOS=linux GOARCH=arm64 CGO_ENABLED=0 go build -ldflags "${LDFLAGS}" -o "${DIST_DIR}/${APP}" . mkdir -p "${package_dir}/docs"
tar -czf "${DIST_DIR}/${APP}_${VERSION}_linux_arm64.tar.gz" -C "${DIST_DIR}" "${APP}" cp docs/guide.md "${package_dir}/docs/"
rm -f "${DIST_DIR}/${APP}" fi
}
normalize_package() {
local package_dir="$1"
find "${package_dir}" -exec touch -h -d "@${SOURCE_DATE_EPOCH}" {} +
}
build_tarball() {
local goos="$1"
local goarch="$2"
local package_dir="${DIST_DIR}/${APP}_${VERSION}_${goos}_${goarch}"
local archive="${DIST_DIR}/${APP}_${VERSION}_${goos}_${goarch}.tar.gz"
echo "==> ${goos}/${goarch}..."
rm -rf "${package_dir}"
mkdir -p "${package_dir}"
GOOS="${goos}" GOARCH="${goarch}" CGO_ENABLED=0 go build -trimpath -ldflags "${LDFLAGS}" -o "${package_dir}/${APP}" .
package_docs "${package_dir}"
normalize_package "${package_dir}"
tar --sort=name --owner=0 --group=0 --numeric-owner --mtime="@${SOURCE_DATE_EPOCH}" -cf - -C "${DIST_DIR}" "$(basename "${package_dir}")" | gzip -n > "${archive}"
rm -rf "${package_dir}"
}
build_zip() {
local goos="$1"
local goarch="$2"
local package_dir="${DIST_DIR}/${APP}_${VERSION}_${goos}_${goarch}"
local archive="${DIST_DIR}/${APP}_${VERSION}_${goos}_${goarch}.zip"
echo "==> ${goos}/${goarch}..."
rm -rf "${package_dir}"
mkdir -p "${package_dir}"
GOOS="${goos}" GOARCH="${goarch}" CGO_ENABLED=0 go build -trimpath -ldflags "${LDFLAGS}" -o "${package_dir}/${APP}.exe" .
package_docs "${package_dir}"
normalize_package "${package_dir}"
(cd "${DIST_DIR}" && find "$(basename "${package_dir}")" -print | sort | zip -X -q "$(basename "${archive}")" -@)
rm -rf "${package_dir}"
}
build_tarball linux amd64
build_tarball linux arm64
build_tarball darwin amd64
build_tarball darwin arm64
build_zip windows amd64
(cd "${DIST_DIR}" && sha256sum *.tar.gz *.zip > checksums.txt)
echo "==> Done." echo "==> Done."
ls -lh "${DIST_DIR}/"*.tar.gz ls -lh "${DIST_DIR}/"*.tar.gz "${DIST_DIR}/"*.zip "${DIST_DIR}/checksums.txt"