Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .github/workflows/beta.yml
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,8 @@ jobs:
- name: Release
run: |
go run ./tools/release notes > "$RUNNER_TEMP/notes.md"
for f in "dist/Flock-$VERSION-macOS.dmg" "dist/Flock-Setup-$VERSION-x64.exe"; do
for f in "dist/Flock-$VERSION-macOS.dmg" "dist/Flock-Setup-$VERSION-x64.exe" \
"dist/flock-$VERSION-linux-x64.tar.gz" "dist/flock-$VERSION-linux-arm64.tar.gz"; do
[ -f "$f" ] || { echo "::error::$f missing"; exit 1; }
done
gh release create "v$VERSION" --repo "$GITHUB_REPOSITORY" --target "$SHA" --prerelease --latest=false \
Expand Down
33 changes: 30 additions & 3 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# Runs the Go tests, and builds the macOS disk image (signed and notarized) and the Windows installer
# (signed with Artifact Signing) and keeps them as run artifacts. Signing takes the secrets and
# Runs the Go tests, and builds the macOS disk image (signed and notarized), the Windows installer
# (signed with Artifact Signing) and the Linux CLI archives, and keeps them as run artifacts. Signing takes the secrets and
# variables of the "release" environment, and is required in the merge queue and for releases.
#
# - on every pull request: the Go tests only; the builds are skipped (which counts as passed)
Expand Down Expand Up @@ -64,7 +64,7 @@ jobs:
fi
echo "version=$version" >> "$GITHUB_OUTPUT"

# The Go tests on both platforms Flock supports (the race detector needs cgo, so macOS only).
# The Go tests on the platforms Flock supports (the race detector needs cgo, so not on Windows).
go-test:
name: Go tests (${{ matrix.name }})
needs: version
Expand All @@ -74,6 +74,8 @@ jobs:
include:
- { name: macOS, os: macos-26, flags: -race }
- { name: Windows, os: windows-2025, flags: "" }
- { name: Linux, os: ubuntu-24.04, flags: -race }
- { name: Linux arm64, os: ubuntu-24.04-arm, flags: -race }
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v7
Expand Down Expand Up @@ -272,3 +274,28 @@ jobs:
name: flock-windows
path: windows/dist/Flock-Setup-*-x64.exe
if-no-files-found: error

linux:
name: Linux (x64, arm64)
needs: version
if: github.event_name != 'pull_request'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
with:
ref: ${{ inputs.ref || github.event.pull_request.head.sha }}

- uses: actions/setup-go@v7
with:
go-version-file: go.mod

- name: Build archives
run: make linux VERSION=${{ needs.version.outputs.version }}

- uses: actions/upload-artifact@v7
with:
name: flock-linux
path: |
dist/flock-*-linux-*.tar.gz
dist/flock-*-linux-*.tar.gz.sha256
if-no-files-found: error
3 changes: 2 additions & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,8 @@ jobs:
VERSION: ${{ needs.release-please.outputs.version }}
run: |
ls dist
for f in "dist/Flock-$VERSION-macOS.dmg" "dist/Flock-Setup-$VERSION-x64.exe"; do
for f in "dist/Flock-$VERSION-macOS.dmg" "dist/Flock-Setup-$VERSION-x64.exe" \
"dist/flock-$VERSION-linux-x64.tar.gz" "dist/flock-$VERSION-linux-arm64.tar.gz"; do
[ -f "$f" ] || { echo "::error::$f missing"; exit 1; }
done
gh release upload "$TAG" dist/* --clobber --repo "$GITHUB_REPOSITORY"
Expand Down
17 changes: 16 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
VERSION ?= $(shell git describe --tags --always --dirty 2>/dev/null || echo dev)
LDFLAGS := -s -w -X flock/internal/cli.Version=$(VERSION)

.PHONY: build build-all test lint notices app dmg windows windows-test
.PHONY: build build-all test lint notices app dmg windows windows-test linux

build:
go build -ldflags "$(LDFLAGS)" -o bin/flock ./cmd/flock

build-all:
GOOS=darwin GOARCH=arm64 go build -ldflags "$(LDFLAGS)" -o dist/flock-darwin-arm64 ./cmd/flock
GOOS=windows GOARCH=amd64 go build -ldflags "$(LDFLAGS)" -o dist/flock-windows-amd64.exe ./cmd/flock
GOOS=linux GOARCH=amd64 go build -ldflags "$(LDFLAGS)" -o dist/flock-linux-amd64 ./cmd/flock
GOOS=linux GOARCH=arm64 go build -ldflags "$(LDFLAGS)" -o dist/flock-linux-arm64 ./cmd/flock

test:
go test -race ./...
Expand All @@ -33,6 +35,19 @@ windows:
GOOS=windows GOARCH=amd64 go build -ldflags "-s -w -X main.version=$(VERSION)" -o dist/windows-amd64/flock-helper.exe ./cmd/flock-helper
GOOS=windows GOARCH=amd64 go build -ldflags "-s -w -H windowsgui" -o dist/windows-amd64/flockd.exe ./cmd/flockd

# The Linux CLI as release archives (install.sh and the AUR package download these):
# dist/flock-<version>-linux-{x64,arm64}.tar.gz with flock-php (util-linux owns flock), each with its .sha256.
linux:
for a in x64:amd64 arm64:arm64; do \
name=flock-$(VERSION)-linux-$${a%%:*}; \
rm -rf dist/$$name && mkdir -p dist/$$name && \
CGO_ENABLED=0 GOOS=linux GOARCH=$${a##*:} go build -trimpath -ldflags "$(LDFLAGS)" -o dist/$$name/flock-php ./cmd/flock && \
cp LICENSE NOTICE THIRD_PARTY_NOTICES dist/$$name/ && \
tar -C dist/$$name -czf dist/$$name.tar.gz flock-php LICENSE NOTICE THIRD_PARTY_NOTICES && \
rm -r dist/$$name && \
(cd dist && sha256sum $$name.tar.gz > $$name.tar.gz.sha256) || exit 1; \
done

# The tray app's models and API client (windows/FlockKit) run on any OS; the app itself is built on Windows (windows/build.ps1).
windows-test:
dotnet test windows/FlockKit.Tests
66 changes: 66 additions & 0 deletions install.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
#!/bin/sh
# Installs Flock on Linux (any distribution with glibc, x86-64 or arm64): downloads the flock CLI of
# the newest release, checks it against its SHA-256 and runs `flock-php install`, which copies it to
# ~/.config/Flock/bin and sets Flock up (asks for your password once). The CLI is flock-php on Linux:
# util-linux owns flock(1).
#
# curl -fsSL https://raw.githubusercontent.com/flock-php/flock/main/install.sh | sh
#
# FLOCK_VERSION=1.0.0 installs that release instead. Arguments go to `flock-php install`:
#
# curl -fsSL https://raw.githubusercontent.com/flock-php/flock/main/install.sh | sh -s -- --skip-privileged
#
# Running it again updates Flock.
set -eu

repo=flock-php/flock

die() {
echo "flock: $*" >&2
exit 1
}

[ "$(uname -s)" = Linux ] || die "this script is for Linux; for macOS and Windows get the app from https://github.com/$repo/releases"

case $(uname -m) in
x86_64 | amd64) arch=x64 ;;
aarch64 | arm64) arch=arm64 ;;
*) die "no Flock build for $(uname -m) (there are x86-64 and arm64 ones)" ;;
esac

for tool in curl tar sha256sum; do
command -v "$tool" >/dev/null || die "$tool is needed (Ubuntu: sudo apt install $tool)"
done

# The tag_name of the first release in a GitHub API response, without its v.
tag() {
sed -n 's/^ *"tag_name": *"v\{0,1\}\([^"]*\)".*/\1/p' | head -n 1
}

version=${FLOCK_VERSION:-}
if [ -z "$version" ]; then
# The newest stable release; until there is one, the newest beta.
version=$(curl -fsSL "https://api.github.com/repos/$repo/releases/latest" 2>/dev/null | tag)
[ -n "$version" ] || version=$(curl -fsSL "https://api.github.com/repos/$repo/releases?per_page=1" | tag)
[ -n "$version" ] || die "could not find a release of $repo"
fi
version=${version#v}

name=flock-$version-linux-$arch.tar.gz
url=https://github.com/$repo/releases/download/v$version/$name

tmp=$(mktemp -d)
trap 'rm -rf "$tmp"' EXIT

echo "Downloading Flock $version ($arch)…"
curl -fsSL -o "$tmp/$name" "$url" || die "could not download $url"
curl -fsSL -o "$tmp/$name.sha256" "$url.sha256" || die "could not download $url.sha256"
(cd "$tmp" && sha256sum -c --status "$name.sha256") || die "$name doesn't match its SHA-256"
tar -xzf "$tmp/$name" -C "$tmp" flock-php

# Piped into sh, stdin is this script: the password prompt and any question need the terminal.
if [ ! -t 0 ] && (: </dev/tty) 2>/dev/null; then
"$tmp/flock-php" install "$@" </dev/tty
else
"$tmp/flock-php" install "$@"
fi
3 changes: 2 additions & 1 deletion internal/api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,8 @@ type DNSStatus struct {
TLD string `json:"tld"`
Addr string `json:"addr"` // the embedded DNS server, when it runs
// Wildcard reports whether every name under the TLD resolves (any subdomain, at any depth).
// When it doesn't, WildcardProblem says why. On Windows (the DNS rule is missing or overridden
// When it doesn't, WildcardProblem says why. On Linux it can also be set while names resolve,
// for what will stop them (Flock's link lost its address, or NetworkManager manages it). On Windows (the DNS rule is missing or overridden
// by a policy) the sites' domains and extra domains still resolve, through the hosts file; on
// macOS (/etc/resolver/<tld> missing or overridden) nothing under the TLD does.
Wildcard bool `json:"wildcard"`
Expand Down
8 changes: 6 additions & 2 deletions internal/binaries/binaries.go
Original file line number Diff line number Diff line change
Expand Up @@ -130,8 +130,8 @@ func (m *Manifest) Versions(pkg, platform string) []Version {
return out
}

// Platform names this machine in the manifest ("macos-arm64", "windows-x64"); "" where Flock
// publishes no packages (Flock supports Apple Silicon Macs and Windows x64).
// Platform names this machine in the manifest ("macos-arm64", "windows-x64", "linux-x64",
// "linux-arm64"); "" where Flock publishes no packages.
func Platform() string {
return platform(runtime.GOOS, runtime.GOARCH)
}
Expand All @@ -142,6 +142,10 @@ func platform(goos, goarch string) string {
return "macos-arm64"
case goos == "windows" && goarch == "amd64":
return "windows-x64"
case goos == "linux" && goarch == "amd64":
return "linux-x64"
case goos == "linux" && goarch == "arm64":
return "linux-arm64"
}
return ""
}
Expand Down
2 changes: 1 addition & 1 deletion internal/binaries/binaries_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -230,7 +230,7 @@ func TestEmbeddedKeyAndPlatforms(t *testing.T) {
if len(PublicKey()) != ed25519.PublicKeySize {
t.Fatal("embedded key")
}
for _, c := range [][3]string{{"darwin", "arm64", "macos-arm64"}, {"darwin", "amd64", ""}, {"windows", "amd64", "windows-x64"}, {"linux", "amd64", ""}} {
for _, c := range [][3]string{{"darwin", "arm64", "macos-arm64"}, {"darwin", "amd64", ""}, {"windows", "amd64", "windows-x64"}, {"linux", "amd64", "linux-x64"}, {"linux", "arm64", "linux-arm64"}, {"linux", "386", ""}} {
if got := platform(c[0], c[1]); got != c[2] {
t.Errorf("%s/%s: %q, want %q", c[0], c[1], got, c[2])
}
Expand Down
23 changes: 21 additions & 2 deletions internal/certs/system_unix.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,27 @@ package certs

import "os"

// bundleFiles are where distributions keep the trust store as one PEM file (the list Go's
// crypto/x509 reads), tried in order. SSL_CERT_FILE, when set, comes first.
var bundleFiles = []string{
"/etc/ssl/certs/ca-certificates.crt", // Debian, Ubuntu, Arch, Gentoo
"/etc/pki/tls/certs/ca-bundle.crt", // Fedora, RHEL
"/etc/ssl/ca-bundle.pem", // openSUSE
"/etc/pki/tls/cacert.pem", // OpenELEC
"/etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem", // CentOS, RHEL 7
"/etc/ssl/cert.pem", // Alpine, the BSDs
}

// SystemRoots returns the OS trust store as PEM, for tools (PHP) that cannot read it themselves.
func SystemRoots() []byte {
data, _ := os.ReadFile("/etc/ssl/cert.pem")
return data
files := bundleFiles
if env := os.Getenv("SSL_CERT_FILE"); env != "" {
files = append([]string{env}, files...)
}
for _, f := range files {
if data, err := os.ReadFile(f); err == nil && len(data) > 0 {
return data
}
}
return nil
}
38 changes: 38 additions & 0 deletions internal/certs/system_unix_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
//go:build !windows && !darwin

package certs

import (
"os"
"path/filepath"
"testing"
)

func TestSystemRootsReadsTheFirstBundleThatExists(t *testing.T) {
dir := t.TempDir()
missing, empty, debian, alpine := filepath.Join(dir, "missing"), filepath.Join(dir, "empty"), filepath.Join(dir, "debian"), filepath.Join(dir, "alpine")
for path, data := range map[string]string{empty: "", debian: "debian roots", alpine: "alpine roots"} {
if err := os.WriteFile(path, []byte(data), 0o644); err != nil {
t.Fatal(err)
}
}
old := bundleFiles
bundleFiles = []string{missing, empty, debian, alpine}
t.Cleanup(func() { bundleFiles = old })
t.Setenv("SSL_CERT_FILE", "")

if got := string(SystemRoots()); got != "debian roots" {
t.Errorf("SystemRoots() = %q, want the Debian bundle", got)
}

t.Setenv("SSL_CERT_FILE", alpine)
if got := string(SystemRoots()); got != "alpine roots" {
t.Errorf("with SSL_CERT_FILE: %q, want its bundle", got)
}

bundleFiles = []string{missing}
t.Setenv("SSL_CERT_FILE", "")
if got := SystemRoots(); got != nil {
t.Errorf("without a bundle: %q, want nil", got)
}
}
8 changes: 6 additions & 2 deletions internal/cli/install.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,15 @@ import (

"flock/internal/certs"
"flock/internal/node"
"flock/internal/paths"
"flock/internal/php"
)

const composerURL = "https://getcomposer.org/download/latest-stable/composer.phar"

// serviceNote is what `flock install` reports once installService is done.
var serviceNote = "daemon runs in the background and starts at login"

func newInstallCommand() *cobra.Command {
var skipPrivileged, privilegedOnly, ignoreConflicts bool

Expand Down Expand Up @@ -97,7 +101,7 @@ func newInstallCommand() *cobra.Command {
if err := installService(s.root, bin); err != nil {
return fmt.Errorf("install background service: %w", err)
}
cmd.Println("✓ daemon runs in the background and starts at login")
cmd.Println("✓ " + serviceNote)

if skipPrivileged || privilegedFirst {
return nil
Expand Down Expand Up @@ -146,7 +150,7 @@ func installBinary(root string) (string, error) {
return "", err
}

dst := filepath.Join(root, "bin", exeName("flock"))
dst := filepath.Join(root, "bin", paths.CLI())
if self == dst {
return dst, nil
}
Expand Down
Loading