mirror of
https://github.com/tailscale/tailscale.git
synced 2025-01-10 10:03:43 +00:00
e711ee5d22
Clamp the min and max version for DSM 7.0 and DSM 7.2 packages when we are building packages for the synology package centre. This change leaves packages destined for pkgs.tailscale.com with just the min version set to not break packages in the wild / our update flow. Updates https://github.com/tailscale/corp/issues/22908 Signed-off-by: Mario Minardi <mario@tailscale.com>
367 lines
9.5 KiB
Go
367 lines
9.5 KiB
Go
// Copyright (c) Tailscale Inc & AUTHORS
|
|
// SPDX-License-Identifier: BSD-3-Clause
|
|
|
|
// Package synology contains dist Targets for building Synology Tailscale packages.
|
|
package synology
|
|
|
|
import (
|
|
"archive/tar"
|
|
"bytes"
|
|
"compress/gzip"
|
|
"embed"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"io/fs"
|
|
"log"
|
|
"os"
|
|
"path/filepath"
|
|
"time"
|
|
|
|
"tailscale.com/release/dist"
|
|
)
|
|
|
|
type target struct {
|
|
filenameArch string
|
|
dsmMajorVersion int
|
|
dsmMinorVersion int
|
|
goenv map[string]string
|
|
packageCenter bool
|
|
signer dist.Signer
|
|
}
|
|
|
|
func (t *target) String() string {
|
|
return fmt.Sprintf("synology/dsm%s/%s", t.dsmVersionString(), t.filenameArch)
|
|
}
|
|
|
|
func (t *target) Build(b *dist.Build) ([]string, error) {
|
|
inner, err := getSynologyBuilds(b).buildInnerPackage(b, t.dsmMajorVersion, t.goenv)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return t.buildSPK(b, inner)
|
|
}
|
|
|
|
// dsmVersionInt combines major and minor version info into an int
|
|
// representation.
|
|
//
|
|
// Version 7.2 becomes 72 as an example.
|
|
func (t *target) dsmVersionInt() int {
|
|
return t.dsmMajorVersion*10 + t.dsmMinorVersion
|
|
}
|
|
|
|
// dsmVersionString returns a string representation of the version
|
|
// including minor version information if it exists.
|
|
//
|
|
// If dsmMinorVersion is 0 this returns dsmMajorVersion as a string,
|
|
// otherwise it returns "dsmMajorVersion-dsmMinorVersion".
|
|
func (t *target) dsmVersionString() string {
|
|
dsmVersionString := fmt.Sprintf("%d", t.dsmMajorVersion)
|
|
if t.dsmMinorVersion != 0 {
|
|
dsmVersionString = fmt.Sprintf("%s-%d", dsmVersionString, t.dsmMinorVersion)
|
|
}
|
|
|
|
return dsmVersionString
|
|
}
|
|
|
|
func (t *target) buildSPK(b *dist.Build, inner *innerPkg) ([]string, error) {
|
|
synoVersion := b.Version.Synology[t.dsmVersionInt()]
|
|
filename := fmt.Sprintf("tailscale-%s-%s-%d-dsm%s.spk", t.filenameArch, b.Version.Short, synoVersion, t.dsmVersionString())
|
|
out := filepath.Join(b.Out, filename)
|
|
if t.packageCenter {
|
|
log.Printf("Building %s (for package center)", filename)
|
|
} else {
|
|
log.Printf("Building %s (for sideloading)", filename)
|
|
}
|
|
|
|
if synoVersion > 2147483647 {
|
|
// Synology requires that version number is within int32 range.
|
|
// Erroring here if we create a build with a higher version.
|
|
// In this case, we'll want to adjust the VersionInfo.Synology logic in
|
|
// the mkversion package.
|
|
return nil, errors.New("syno version exceeds int32 range")
|
|
}
|
|
|
|
privFile := fmt.Sprintf("privilege-dsm%d", t.dsmMajorVersion)
|
|
if t.packageCenter && t.dsmMajorVersion == 7 {
|
|
privFile += ".for-package-center"
|
|
}
|
|
|
|
f, err := os.Create(out)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer f.Close()
|
|
tw := tar.NewWriter(f)
|
|
defer tw.Close()
|
|
|
|
err = writeTar(tw, b.Time,
|
|
memFile("INFO", t.mkInfo(b, inner.uncompressedSz), 0644),
|
|
static("PACKAGE_ICON.PNG", "PACKAGE_ICON.PNG", 0644),
|
|
static("PACKAGE_ICON_256.PNG", "PACKAGE_ICON_256.PNG", 0644),
|
|
static("Tailscale.sc", "Tailscale.sc", 0644),
|
|
dir("conf"),
|
|
static("resource", "conf/resource", 0644),
|
|
static(privFile, "conf/privilege", 0644),
|
|
file(inner.path, "package.tgz", 0644),
|
|
dir("scripts"),
|
|
static("scripts/start-stop-status", "scripts/start-stop-status", 0644),
|
|
static("scripts/postupgrade", "scripts/postupgrade", 0644),
|
|
static("scripts/preupgrade", "scripts/preupgrade", 0644),
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if err := tw.Close(); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := f.Close(); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
files := []string{out}
|
|
|
|
if t.signer != nil {
|
|
outSig := out + ".sig"
|
|
if err := t.signer.SignFile(out, outSig); err != nil {
|
|
return nil, err
|
|
}
|
|
files = append(files, outSig)
|
|
}
|
|
|
|
return files, nil
|
|
}
|
|
|
|
func (t *target) mkInfo(b *dist.Build, uncompressedSz int64) []byte {
|
|
var ret bytes.Buffer
|
|
f := func(k, v string) {
|
|
fmt.Fprintf(&ret, "%s=%q\n", k, v)
|
|
}
|
|
f("package", "Tailscale")
|
|
f("version", fmt.Sprintf("%s-%d", b.Version.Short, b.Version.Synology[t.dsmVersionInt()]))
|
|
f("arch", t.filenameArch)
|
|
f("description", "Connect all your devices using WireGuard, without the hassle.")
|
|
f("displayname", "Tailscale")
|
|
f("maintainer", "Tailscale, Inc.")
|
|
f("maintainer_url", "https://github.com/tailscale/tailscale")
|
|
f("create_time", b.Time.Format("20060102-15:04:05"))
|
|
f("dsmuidir", "ui")
|
|
f("dsmappname", "SYNO.SDS.Tailscale")
|
|
f("startstop_restart_services", "nginx")
|
|
switch t.dsmMajorVersion {
|
|
case 6:
|
|
f("os_min_ver", "6.0.1-7445")
|
|
f("os_max_ver", "7.0-40000")
|
|
case 7:
|
|
if t.packageCenter {
|
|
switch t.dsmMinorVersion {
|
|
case 0:
|
|
f("os_min_ver", "7.0-40000")
|
|
f("os_max_ver", "7.2-60000")
|
|
case 2:
|
|
f("os_min_ver", "7.2-60000")
|
|
default:
|
|
panic(fmt.Sprintf("unsupported DSM major.minor version %s", t.dsmVersionString()))
|
|
}
|
|
} else {
|
|
// We do not clamp the os_max_ver currently for non-package center builds as
|
|
// the binaries for 7.0 and 7.2 are identical.
|
|
f("os_min_ver", "7.0-40000")
|
|
f("os_max_ver", "")
|
|
}
|
|
default:
|
|
panic(fmt.Sprintf("unsupported DSM major version %d", t.dsmMajorVersion))
|
|
}
|
|
f("extractsize", fmt.Sprintf("%v", uncompressedSz>>10)) // in KiB
|
|
return ret.Bytes()
|
|
}
|
|
|
|
type synologyBuildsMemoizeKey struct{}
|
|
|
|
type innerPkg struct {
|
|
path string
|
|
uncompressedSz int64
|
|
}
|
|
|
|
// synologyBuilds is extra build context shared by all synology builds.
|
|
type synologyBuilds struct {
|
|
innerPkgs dist.Memoize[*innerPkg]
|
|
}
|
|
|
|
// getSynologyBuilds returns the synologyBuilds for b, creating one if needed.
|
|
func getSynologyBuilds(b *dist.Build) *synologyBuilds {
|
|
return b.Extra(synologyBuildsMemoizeKey{}, func() any { return new(synologyBuilds) }).(*synologyBuilds)
|
|
}
|
|
|
|
// buildInnerPackage builds the inner tarball for synology packages,
|
|
// which contains the files to unpack to disk on installation (as
|
|
// opposed to the outer tarball, which contains package metadata)
|
|
func (m *synologyBuilds) buildInnerPackage(b *dist.Build, dsmVersion int, goenv map[string]string) (*innerPkg, error) {
|
|
key := []any{dsmVersion, goenv}
|
|
return m.innerPkgs.Do(key, func() (*innerPkg, error) {
|
|
if err := b.BuildWebClientAssets(); err != nil {
|
|
return nil, err
|
|
}
|
|
ts, err := b.BuildGoBinary("tailscale.com/cmd/tailscale", goenv)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
tsd, err := b.BuildGoBinary("tailscale.com/cmd/tailscaled", goenv)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
tmp := b.TmpDir()
|
|
out := filepath.Join(tmp, "package.tgz")
|
|
|
|
f, err := os.Create(out)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer f.Close()
|
|
gw := gzip.NewWriter(f)
|
|
defer gw.Close()
|
|
cw := &countingWriter{gw, 0}
|
|
tw := tar.NewWriter(cw)
|
|
defer tw.Close()
|
|
|
|
err = writeTar(tw, b.Time,
|
|
dir("bin"),
|
|
file(tsd, "bin/tailscaled", 0755),
|
|
file(ts, "bin/tailscale", 0755),
|
|
dir("conf"),
|
|
static("Tailscale.sc", "conf/Tailscale.sc", 0644),
|
|
static(fmt.Sprintf("logrotate-dsm%d", dsmVersion), "conf/logrotate.conf", 0644),
|
|
dir("ui"),
|
|
static("PACKAGE_ICON_256.PNG", "ui/PACKAGE_ICON_256.PNG", 0644),
|
|
static("config", "ui/config", 0644),
|
|
static("index.cgi", "ui/index.cgi", 0755))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if err := tw.Close(); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := gw.Close(); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := f.Close(); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &innerPkg{out, cw.n}, nil
|
|
})
|
|
}
|
|
|
|
// writeTar writes ents to tw.
|
|
func writeTar(tw *tar.Writer, modTime time.Time, ents ...tarEntry) error {
|
|
for _, ent := range ents {
|
|
if err := ent(tw, modTime); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// tarEntry is a function that writes tar entries (files or
|
|
// directories) to a tar.Writer.
|
|
type tarEntry func(*tar.Writer, time.Time) error
|
|
|
|
// fsFile returns a tarEntry that writes src in fsys to dst in the tar
|
|
// file, with mode.
|
|
func fsFile(fsys fs.FS, src, dst string, mode int64) tarEntry {
|
|
return func(tw *tar.Writer, modTime time.Time) error {
|
|
f, err := fsys.Open(src)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer f.Close()
|
|
fi, err := f.Stat()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
hdr := &tar.Header{
|
|
Name: dst,
|
|
Size: fi.Size(),
|
|
Mode: mode,
|
|
ModTime: modTime,
|
|
}
|
|
if err := tw.WriteHeader(hdr); err != nil {
|
|
return err
|
|
}
|
|
if _, err = io.Copy(tw, f); err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// file returns a tarEntry that writes src on disk into the tar file as
|
|
// dst, with mode.
|
|
func file(src, dst string, mode int64) tarEntry {
|
|
return fsFile(os.DirFS(filepath.Dir(src)), filepath.Base(src), dst, mode)
|
|
}
|
|
|
|
//go:embed files/*
|
|
var files embed.FS
|
|
|
|
// static returns a tarEntry that writes src in files/ into the tar
|
|
// file as dst, with mode.
|
|
func static(src, dst string, mode int64) tarEntry {
|
|
fsys, err := fs.Sub(files, "files")
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
return fsFile(fsys, src, dst, mode)
|
|
}
|
|
|
|
// memFile returns a tarEntry that writes bs to dst in the tar file,
|
|
// with mode.
|
|
func memFile(dst string, bs []byte, mode int64) tarEntry {
|
|
return func(tw *tar.Writer, modTime time.Time) error {
|
|
hdr := &tar.Header{
|
|
Name: dst,
|
|
Size: int64(len(bs)),
|
|
Mode: mode,
|
|
ModTime: modTime,
|
|
}
|
|
if err := tw.WriteHeader(hdr); err != nil {
|
|
return err
|
|
}
|
|
if _, err := tw.Write(bs); err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// dir returns a tarEntry that creates a world-readable directory in
|
|
// the tar file.
|
|
func dir(name string) tarEntry {
|
|
return func(tw *tar.Writer, modTime time.Time) error {
|
|
return tw.WriteHeader(&tar.Header{
|
|
Typeflag: tar.TypeDir,
|
|
Name: name + "/",
|
|
Mode: 0755,
|
|
ModTime: modTime,
|
|
// TODO: why tailscale? Files are being written as owned by root.
|
|
Uname: "tailscale",
|
|
Gname: "tailscale",
|
|
})
|
|
}
|
|
}
|
|
|
|
type countingWriter struct {
|
|
w io.Writer
|
|
n int64
|
|
}
|
|
|
|
func (cw *countingWriter) Write(bs []byte) (int, error) {
|
|
n, err := cw.w.Write(bs)
|
|
cw.n += int64(n)
|
|
return n, err
|
|
}
|