// Package runnerdist serves the quarry-runner program for download, so
// a repository's owners can run its CI on their own machine without
// building anything: the forge carries a build of the runner for each
// platform, made alongside the forge itself, and the setup page links
// to it.
package runnerdist
import (
"net/http"
"os"
"path/filepath"
"regexp"
)
// Platform is one downloadable build.
type Platform struct {
OS, Arch, Label string
}
// File is the name the platform's build is served and saved as.
func (p Platform) File() string {
name := "quarry-runner-" + p.OS + "-" + p.Arch
if p.OS == "windows" {
name += ".exe"
}
return name
}
// Platforms is every build the forge carries, in the order a setup page
// lists them. scripts/build-linux.sh builds exactly these.
var Platforms = []Platform{
{"linux", "amd64", "Linux x86-64"},
{"linux", "arm64", "Linux ARM64"},
{"darwin", "arm64", "macOS Apple silicon"},
{"darwin", "amd64", "macOS Intel"},
{"windows", "amd64", "Windows x86-64"},
}
// SumsFile is the checksum list deploy publishes beside the builds, in
// sha256sum's own format, so `sha256sum -c --ignore-missing` checks a
// download against it.
const SumsFile = "SHA256SUMS"
var fileName = regexp.MustCompile(`^quarry-runner-(linux|darwin|windows)-(amd64|arm64)(\.exe)?$`)
// Handler serves GET /runner/{file} from dir. Only names shaped like a
// build are looked up, so the route can never reach another file in
// dir or outside it.
func Handler(dir string) http.Handler {
mux := http.NewServeMux()
mux.HandleFunc("GET /runner/{file}", func(w http.ResponseWriter, req *http.Request) {
name := req.PathValue("file")
if name != SumsFile && !fileName.MatchString(name) {
http.NotFound(w, req)
return
}
f, err := os.Open(filepath.Join(dir, name))
if err != nil {
http.Error(w, "this forge carries no "+name+" build", http.StatusNotFound)
return
}
defer f.Close()
st, err := f.Stat()
if err != nil || st.IsDir() {
http.NotFound(w, req)
return
}
if name == SumsFile {
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
} else {
w.Header().Set("Content-Type", "application/octet-stream")
w.Header().Set("Content-Disposition", `attachment; filename="`+name+`"`)
}
w.Header().Set("X-Content-Type-Options", "nosniff")
// Each deploy replaces the build; revalidate rather than keep a
// runner that no longer matches the forge it talks to.
w.Header().Set("Cache-Control", "no-cache")
http.ServeContent(w, req, name, st.ModTime(), f)
})
return mux
}
// DefaultDir is where the builds live when no flag says: runner/ beside
// the quarry binary, which is where deploy installs them.
func DefaultDir() string {
exe, err := os.Executable()
if err != nil {
return "runner"
}
return filepath.Join(filepath.Dir(exe), "runner")
}