Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
fb4e657
fix(certs): read the system CA bundle where Linux distributions keep it
sineld Oct 18, 2026
18df29e
feat(binaries): name linux-x64 and linux-arm64 in the manifest
sineld Oct 18, 2026
d06e02b
feat(daemon): check on Linux that .test lookups reach Flock's DNS server
sineld Oct 18, 2026
8d4291e
fix(ports): a low port without permission isn't a taken one on Linux
sineld Oct 18, 2026
ecd6f1c
refactor(cli): share the shims and shell profile code of macOS with L…
sineld Oct 18, 2026
df13d6c
feat(cli): flock install, uninstall and restart on Linux
sineld Oct 18, 2026
befd5e3
ci: run the Go tests on Linux and build the CLI for it
sineld Oct 19, 2026
c195af1
fix(ports): find a port's holder through /proc on Linux, without lsof
sineld Oct 19, 2026
838ed20
fix(cli): name the Linux CLI flock-php, util-linux owns flock(1)
sineld Oct 19, 2026
043d519
fix(dns): route the TLD through a link of Flock's own on Linux
sineld Oct 19, 2026
18d4050
fix(dns): keep NetworkManager off flock0, and report the link when it…
sineld Oct 19, 2026
9d631ee
fix(dns): check flock0 and NetworkManager even while lookups work, so…
sineld Oct 19, 2026
31d1957
feat(linux): release archives of the Linux CLI, and install.sh that i…
Nejcc Oct 19, 2026
957985f
feat(linux): AUR package flock-php-bin
Nejcc Oct 19, 2026
89d0784
fix(linux): ship the CLI as flock-php, as #12 now names it
Nejcc Oct 19, 2026
bba647a
test(linux): an end-to-end test of Flock on Arch, Ubuntu and Fedora
Nejcc Oct 19, 2026
de949b6
test(linux): check flock0's NetworkManager state in the test's own shell
Nejcc Oct 19, 2026
ebfdb53
test(linux): fail on a failed install, and on a dead daemon's status
Nejcc Oct 19, 2026
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
53 changes: 53 additions & 0 deletions .github/workflows/linux-e2e.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# The Linux end-to-end test (tools/linux-e2e/run.sh): Flock installed with install.sh, checked
# (DNS through flock0, NetworkManager, drift, HTTPS, a reboot) and uninstalled, in Arch, Ubuntu and
# Fedora containers booting systemd. Runs when the Linux setup changes, and by hand.
name: Linux e2e

on:
workflow_dispatch:
pull_request:
paths:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The drift report this test guards lives in internal/daemon/wildcard.go: checkWildcard calls dns.LinkProblem on Linux. internal/daemon/** isn't in the list, so a change there that broke the report wouldn't run the test. go.mod/go.sum (and maybe cmd/flock/**) may be worth adding too.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added internal/daemon/**, cmd/flock/**, go.mod and go.sum in ebfdb53. I'd missed that checkWildcard → dns.LinkProblem path.

- internal/cli/**
- internal/dns/**
- internal/certs/**
- internal/ports/**
- internal/daemon/**
- cmd/flock/**
- go.mod
- go.sum
- install.sh
- Makefile
- tools/linux-e2e/**
- .github/workflows/linux-e2e.yml

permissions:
contents: read

concurrency:
group: linux-e2e-${{ github.event.pull_request.number || github.run_id }}
cancel-in-progress: true

jobs:
e2e:
name: ${{ matrix.distro }}
strategy:
fail-fast: false
matrix:
distro: [arch, ubuntu, fedora]
runs-on: ubuntu-24.04
timeout-minutes: 30
steps:
- uses: actions/checkout@v7
with:
ref: ${{ github.event.pull_request.head.sha }}

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

# flock0 is a dummy link; a rootless container can't load the module itself.
- name: Load the dummy module
run: sudo modprobe dummy numdummies=0

- name: Run
run: tools/linux-e2e/run.sh ${{ matrix.distro }}
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