From 32e0ba5e686a3db4b3a5acceb1e09e9de3f9a2ae Mon Sep 17 00:00:00 2001 From: David Anderson Date: Fri, 26 May 2023 12:42:05 -0700 Subject: [PATCH] release/dist/synology: build synology packages with cmd/dist Updates #8217 Signed-off-by: David Anderson --- Makefile | 5 +- cmd/dist/dist.go | 25 +- release/dist/dist.go | 7 + release/dist/synology/files/PACKAGE_ICON.PNG | Bin 0 -> 4011 bytes .../dist/synology/files/PACKAGE_ICON_256.PNG | Bin 0 -> 26481 bytes release/dist/synology/files/Tailscale.sc | 6 + release/dist/synology/files/config | 12 + release/dist/synology/files/index.cgi | 2 + release/dist/synology/files/logrotate-dsm6 | 8 + release/dist/synology/files/logrotate-dsm7 | 8 + release/dist/synology/files/privilege-dsm6 | 7 + release/dist/synology/files/privilege-dsm7 | 7 + .../files/privilege-dsm7.for-package-center | 13 + release/dist/synology/files/resource | 11 + .../dist/synology/files/scripts/postupgrade | 3 + .../dist/synology/files/scripts/preupgrade | 3 + .../synology/files/scripts/start-stop-status | 129 ++++++++ release/dist/synology/pkgs.go | 306 ++++++++++++++++++ release/dist/synology/targets.go | 69 ++++ release/dist/unixpkgs/pkgs.go | 6 +- tool/gocross/autoflags.go | 4 +- version/mkversion/mkversion.go | 8 + 22 files changed, 630 insertions(+), 9 deletions(-) create mode 100644 release/dist/synology/files/PACKAGE_ICON.PNG create mode 100644 release/dist/synology/files/PACKAGE_ICON_256.PNG create mode 100644 release/dist/synology/files/Tailscale.sc create mode 100644 release/dist/synology/files/config create mode 100755 release/dist/synology/files/index.cgi create mode 100644 release/dist/synology/files/logrotate-dsm6 create mode 100644 release/dist/synology/files/logrotate-dsm7 create mode 100644 release/dist/synology/files/privilege-dsm6 create mode 100644 release/dist/synology/files/privilege-dsm7 create mode 100644 release/dist/synology/files/privilege-dsm7.for-package-center create mode 100644 release/dist/synology/files/resource create mode 100644 release/dist/synology/files/scripts/postupgrade create mode 100644 release/dist/synology/files/scripts/preupgrade create mode 100755 release/dist/synology/files/scripts/start-stop-status create mode 100644 release/dist/synology/pkgs.go create mode 100644 release/dist/synology/targets.go diff --git a/Makefile b/Makefile index 1c26f0451..4a9acb0b4 100644 --- a/Makefile +++ b/Makefile @@ -48,11 +48,10 @@ staticcheck: ## Run staticcheck.io checks ./tool/go run honnef.co/go/tools/cmd/staticcheck -- $$(./tool/go list ./... | grep -v tempfork) spk: ## Build synology package for ${SYNO_ARCH} architecture and ${SYNO_DSM} DSM version - PATH="${PWD}/tool:${PATH}" ./tool/go run github.com/tailscale/tailscale-synology@main -o tailscale.spk --source=. --goarch=${SYNO_ARCH} --dsm-version=${SYNO_DSM} + ./tool/go run ./cmd/dist build synology/dsm${SYNO_DSM}/${SYNO_ARCH} spkall: ## Build synology packages for all architectures and DSM versions - mkdir -p spks - PATH="${PWD}/tool:${PATH}" ./tool/go run github.com/tailscale/tailscale-synology@main -o spks --source=. --goarch=all --dsm-version=all + ./tool/go run ./cmd/dist build synology pushspk: spk ## Push and install synology package on ${SYNO_HOST} host echo "Pushing SPK to root@${SYNO_HOST} (env var SYNO_HOST) ..." diff --git a/cmd/dist/dist.go b/cmd/dist/dist.go index 04d1c57a9..c184e6700 100644 --- a/cmd/dist/dist.go +++ b/cmd/dist/dist.go @@ -13,15 +13,38 @@ import ( "tailscale.com/release/dist" "tailscale.com/release/dist/cli" + "tailscale.com/release/dist/synology" "tailscale.com/release/dist/unixpkgs" ) +var synologyPackageCenter bool + func getTargets() ([]dist.Target, error) { - return unixpkgs.Targets(), nil + var ret []dist.Target + + ret = append(ret, unixpkgs.Targets()...) + // Synology packages can be built either for sideloading, or for + // distribution by Synology in their package center. When + // distributed through the package center, apps can request + // additional permissions to use a tuntap interface and control + // the NAS's network stack, rather than be forced to run in + // userspace mode. + // + // Since only we can provide packages to Synology for + // distribution, we default to building the "sideload" variant of + // packages that we distribute on pkgs.tailscale.com. + ret = append(ret, synology.Targets(synologyPackageCenter)...) + return ret, nil } func main() { cmd := cli.CLI(getTargets) + for _, subcmd := range cmd.Subcommands { + if subcmd.Name == "build" { + subcmd.FlagSet.BoolVar(&synologyPackageCenter, "synology-package-center", false, "build synology packages with extra metadata for the official package center") + } + } + if err := cmd.ParseAndRun(context.Background(), os.Args[1:]); err != nil && !errors.Is(err, flag.ErrHelp) { log.Fatal(err) } diff --git a/release/dist/dist.go b/release/dist/dist.go index e3fe848a9..fcd34609c 100644 --- a/release/dist/dist.go +++ b/release/dist/dist.go @@ -17,6 +17,7 @@ import ( "sort" "strings" "sync" + "time" "tailscale.com/util/multierr" "tailscale.com/version/mkversion" @@ -44,6 +45,8 @@ type Build struct { Go string // Version is the version info of the build. Version mkversion.VersionInfo + // Time is the timestamp of the build. + Time time.Time // once is a cache of function invocations that should run once per process // (for example building a helper docker container) @@ -86,6 +89,7 @@ func NewBuild(repo, out string) (*Build, error) { Out: out, Go: goTool, Version: mkversion.Info(), + Time: time.Now().UTC(), extra: map[any]any{}, goBuildLimit: make(chan struct{}, runtime.NumCPU()), } @@ -114,6 +118,9 @@ func (b *Build) Build(targets []Target) (files []string, err error) { go func(i int, t Target) { var err error defer func() { + if err != nil { + err = fmt.Errorf("%s: %w", t, err) + } errs[i] = err wg.Done() }() diff --git a/release/dist/synology/files/PACKAGE_ICON.PNG b/release/dist/synology/files/PACKAGE_ICON.PNG new file mode 100644 index 0000000000000000000000000000000000000000..67b09b5d2f05c956655329b23e342ad91072cc60 GIT binary patch literal 4011 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 R%wYfk002ovPDHLkV1nn_?{)wH literal 0 HcmV?d00001 diff --git a/release/dist/synology/files/PACKAGE_ICON_256.PNG b/release/dist/synology/files/PACKAGE_ICON_256.PNG new file mode 100644 index 0000000000000000000000000000000000000000..927b5f8f4d680e43b24b9b32f9645fcc38edc82d GIT binary patch literal 26481 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 KrFuo1sQ&?`m<`eZ literal 0 HcmV?d00001 diff --git a/release/dist/synology/files/Tailscale.sc b/release/dist/synology/files/Tailscale.sc new file mode 100644 index 000000000..707ac6bb0 --- /dev/null +++ b/release/dist/synology/files/Tailscale.sc @@ -0,0 +1,6 @@ +[Tailscale] +title="Tailscale" +desc="Tailscale VPN" +port_forward="no" +src.ports="41641/udp" +dst.ports="41641/udp" \ No newline at end of file diff --git a/release/dist/synology/files/config b/release/dist/synology/files/config new file mode 100644 index 000000000..7bd404db2 --- /dev/null +++ b/release/dist/synology/files/config @@ -0,0 +1,12 @@ +{ + ".url": { + "SYNO.SDS.Tailscale": { + "type": "url", + "version": "1.8.3", + "title": "Tailscale", + "icon": "PACKAGE_ICON_256.PNG", + "url": "webman/3rdparty/Tailscale/", + "urlTarget": "_syno_tailscale" + } + } +} diff --git a/release/dist/synology/files/index.cgi b/release/dist/synology/files/index.cgi new file mode 100755 index 000000000..7682824ec --- /dev/null +++ b/release/dist/synology/files/index.cgi @@ -0,0 +1,2 @@ +#! /bin/sh +exec /var/packages/Tailscale/target/bin/tailscale web -cgi \ No newline at end of file diff --git a/release/dist/synology/files/logrotate-dsm6 b/release/dist/synology/files/logrotate-dsm6 new file mode 100644 index 000000000..2df64283a --- /dev/null +++ b/release/dist/synology/files/logrotate-dsm6 @@ -0,0 +1,8 @@ +/var/packages/Tailscale/etc/tailscaled.stdout.log { + size 10M + rotate 3 + missingok + copytruncate + compress + notifempty +} diff --git a/release/dist/synology/files/logrotate-dsm7 b/release/dist/synology/files/logrotate-dsm7 new file mode 100644 index 000000000..7020dc925 --- /dev/null +++ b/release/dist/synology/files/logrotate-dsm7 @@ -0,0 +1,8 @@ +/var/packages/Tailscale/var/tailscaled.stdout.log { + size 10M + rotate 3 + missingok + copytruncate + compress + notifempty +} diff --git a/release/dist/synology/files/privilege-dsm6 b/release/dist/synology/files/privilege-dsm6 new file mode 100644 index 000000000..4b6fe093a --- /dev/null +++ b/release/dist/synology/files/privilege-dsm6 @@ -0,0 +1,7 @@ +{ + "defaults":{ + "run-as": "root" + }, + "username": "tailscale", + "groupname": "tailscale" +} diff --git a/release/dist/synology/files/privilege-dsm7 b/release/dist/synology/files/privilege-dsm7 new file mode 100644 index 000000000..93a9c4f7d --- /dev/null +++ b/release/dist/synology/files/privilege-dsm7 @@ -0,0 +1,7 @@ +{ + "defaults":{ + "run-as": "package" + }, + "username": "tailscale", + "groupname": "tailscale" +} diff --git a/release/dist/synology/files/privilege-dsm7.for-package-center b/release/dist/synology/files/privilege-dsm7.for-package-center new file mode 100644 index 000000000..db1468346 --- /dev/null +++ b/release/dist/synology/files/privilege-dsm7.for-package-center @@ -0,0 +1,13 @@ +{ + "defaults":{ + "run-as": "package" + }, + "username": "tailscale", + "groupname": "tailscale", + "tool": [{ + "relpath": "bin/tailscaled", + "user": "package", + "group": "package", + "capabilities": "cap_net_admin,cap_chown,cap_net_raw" + }] +} diff --git a/release/dist/synology/files/resource b/release/dist/synology/files/resource new file mode 100644 index 000000000..0da0002ef --- /dev/null +++ b/release/dist/synology/files/resource @@ -0,0 +1,11 @@ +{ + "port-config": { + "protocol-file": "conf/Tailscale.sc" + }, + "usr-local-linker": { + "bin": ["bin/tailscale"] + }, + "syslog-config": { + "logrotate-relpath": "conf/logrotate.conf" + } +} \ No newline at end of file diff --git a/release/dist/synology/files/scripts/postupgrade b/release/dist/synology/files/scripts/postupgrade new file mode 100644 index 000000000..92b94c40c --- /dev/null +++ b/release/dist/synology/files/scripts/postupgrade @@ -0,0 +1,3 @@ +#!/bin/sh + +exit 0 \ No newline at end of file diff --git a/release/dist/synology/files/scripts/preupgrade b/release/dist/synology/files/scripts/preupgrade new file mode 100644 index 000000000..92b94c40c --- /dev/null +++ b/release/dist/synology/files/scripts/preupgrade @@ -0,0 +1,3 @@ +#!/bin/sh + +exit 0 \ No newline at end of file diff --git a/release/dist/synology/files/scripts/start-stop-status b/release/dist/synology/files/scripts/start-stop-status new file mode 100755 index 000000000..e6ece04e3 --- /dev/null +++ b/release/dist/synology/files/scripts/start-stop-status @@ -0,0 +1,129 @@ +#!/bin/bash + +SERVICE_NAME="tailscale" + +if [ "${SYNOPKG_DSM_VERSION_MAJOR}" -eq "6" ]; then + PKGVAR="/var/packages/Tailscale/etc" +else + PKGVAR="${SYNOPKG_PKGVAR}" +fi + +PID_FILE="${PKGVAR}/tailscaled.pid" +LOG_FILE="${PKGVAR}/tailscaled.stdout.log" +STATE_FILE="${PKGVAR}/tailscaled.state" +SOCKET_FILE="${PKGVAR}/tailscaled.sock" +PORT="41641" + +SERVICE_COMMAND="${SYNOPKG_PKGDEST}/bin/tailscaled \ +--state=${STATE_FILE} \ +--socket=${SOCKET_FILE} \ +--port=$PORT" + +if [ "${SYNOPKG_DSM_VERSION_MAJOR}" -eq "7" -a ! -e "/dev/net/tun" ]; then + # TODO(maisem/crawshaw): Disable the tun device in DSM7 for now. + SERVICE_COMMAND="${SERVICE_COMMAND} --tun=userspace-networking" +fi + +if [ "${SYNOPKG_DSM_VERSION_MAJOR}" -eq "6" ]; then + chown -R tailscale:tailscale "${PKGVAR}/" +fi + +start_daemon() { + local ts=$(date --iso-8601=second) + echo "${ts} Starting ${SERVICE_NAME} with: ${SERVICE_COMMAND}" >${LOG_FILE} + STATE_DIRECTORY=${PKGVAR} ${SERVICE_COMMAND} 2>&1 | sed -u '1,200p;201s,.*,[further tailscaled logs suppressed],p;d' >>${LOG_FILE} & + # We pipe tailscaled's output to sed, so "$!" retrieves the PID of sed not tailscaled. + # Use jobs -p to retrieve the PID of the most recent process group leader. + jobs -p >"${PID_FILE}" +} + +stop_daemon() { + if [ -r "${PID_FILE}" ]; then + local PID=$(cat "${PID_FILE}") + local ts=$(date --iso-8601=second) + echo "${ts} Stopping ${SERVICE_NAME} service PID=${PID}" >>${LOG_FILE} + kill -TERM $PID >>${LOG_FILE} 2>&1 + wait_for_status 1 || kill -KILL $PID >>${LOG_FILE} 2>&1 + rm -f "${PID_FILE}" >/dev/null + fi +} + +daemon_status() { + if [ -r "${PID_FILE}" ]; then + local PID=$(cat "${PID_FILE}") + if ps -o pid -p ${PID} > /dev/null; then + return + fi + rm -f "${PID_FILE}" >/dev/null + fi + return 1 +} + +wait_for_status() { + # 20 tries + # sleeps for 1 second after each try + local counter=20 + while [ ${counter} -gt 0 ]; do + daemon_status + [ $? -eq $1 ] && return + counter=$((counter - 1)) + sleep 1 + done + return 1 +} + +ensure_tun_created() { + if [ "${SYNOPKG_DSM_VERSION_MAJOR}" -eq "7" ]; then + # TODO(maisem/crawshaw): Disable the tun device in DSM7 for now. + return + fi + # Create the necessary file structure for /dev/net/tun + if ([ ! -c /dev/net/tun ]); then + if ([ ! -d /dev/net ]); then + mkdir -m 755 /dev/net + fi + mknod /dev/net/tun c 10 200 + chmod 0755 /dev/net/tun + fi + + # Load the tun module if not already loaded + if (!(lsmod | grep -q "^tun\s")); then + insmod /lib/modules/tun.ko + fi +} + +case $1 in +start) + if daemon_status; then + exit 0 + else + ensure_tun_created + start_daemon + exit $? + fi + ;; +stop) + if daemon_status; then + stop_daemon + exit $? + else + exit 0 + fi + ;; +status) + if daemon_status; then + echo "${SERVICE_NAME} is running" + exit 0 + else + echo "${SERVICE_NAME} is not running" + exit 3 + fi + ;; +log) + exit 0 + ;; +*) + echo "command $1 is not implemented" + exit 0 + ;; +esac diff --git a/release/dist/synology/pkgs.go b/release/dist/synology/pkgs.go new file mode 100644 index 000000000..86d83d84b --- /dev/null +++ b/release/dist/synology/pkgs.go @@ -0,0 +1,306 @@ +// 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" + "fmt" + "io" + "io/fs" + "log" + "os" + "path/filepath" + "time" + + "tailscale.com/release/dist" +) + +type target struct { + filenameArch string + dsmMajorVersion int + goenv map[string]string + packageCenter bool +} + +func (t *target) String() string { + return fmt.Sprintf("synology/dsm%d/%s", t.dsmMajorVersion, 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 + } + + out, err := t.buildSPK(b, inner) + if err != nil { + return nil, err + } + + return []string{out}, nil +} + +func (t *target) buildSPK(b *dist.Build, inner *innerPkg) (string, error) { + filename := fmt.Sprintf("tailscale-%s-%s-%d-dsm%d.spk", t.filenameArch, b.Version.Short, b.Version.Synology[t.dsmMajorVersion], t.dsmMajorVersion) + out := filepath.Join(b.Out, filename) + log.Printf("Building %s", filename) + + 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 "", 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 "", err + } + + if err := tw.Close(); err != nil { + return "", err + } + if err := f.Close(); err != nil { + return "", err + } + + return out, 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.dsmMajorVersion])) + 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: + 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) { + 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), // TODO: this has "1.8.3" hard-coded in it; why? what is it? bug? + 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 +} diff --git a/release/dist/synology/targets.go b/release/dist/synology/targets.go new file mode 100644 index 000000000..3ef346c1e --- /dev/null +++ b/release/dist/synology/targets.go @@ -0,0 +1,69 @@ +// Copyright (c) Tailscale Inc & AUTHORS +// SPDX-License-Identifier: BSD-3-Clause + +package synology + +import "tailscale.com/release/dist" + +func Targets(forPackageCenter bool) []dist.Target { + var ret []dist.Target + for _, dsmVersion := range []int{6, 7} { + ret = append(ret, + &target{ + filenameArch: "x86_64", + dsmMajorVersion: dsmVersion, + goenv: map[string]string{ + "GOOS": "linux", + "GOARCH": "amd64", + }, + packageCenter: forPackageCenter, + }, + &target{ + filenameArch: "i686", + dsmMajorVersion: dsmVersion, + goenv: map[string]string{ + "GOOS": "linux", + "GOARCH": "386", + }, + packageCenter: forPackageCenter, + }, + &target{ + filenameArch: "armv8", + dsmMajorVersion: dsmVersion, + goenv: map[string]string{ + "GOOS": "linux", + "GOARCH": "arm64", + }, + packageCenter: forPackageCenter, + }) + + // On older ARMv5 and ARMv7 platforms, synology used a whole + // mess of SoC-specific target names, even though the packages + // built for each are identical apart from metadata. + for _, v5Arch := range []string{"armv5", "88f6281", "88f6282"} { + ret = append(ret, &target{ + filenameArch: v5Arch, + dsmMajorVersion: dsmVersion, + goenv: map[string]string{ + "GOOS": "linux", + "GOARCH": "arm", + "GOARM": "5", + }, + packageCenter: forPackageCenter, + }) + } + for _, v7Arch := range []string{"armv7", "alpine", "armada370", "armada375", "armada38x", "armadaxp", "comcerto2k", "monaco", "hi3535"} { + ret = append(ret, &target{ + filenameArch: v7Arch, + dsmMajorVersion: dsmVersion, + goenv: map[string]string{ + "GOOS": "linux", + "GOARCH": "arm", + "GOARM": "7", + }, + packageCenter: forPackageCenter, + }) + } + } + return ret +} diff --git a/release/dist/unixpkgs/pkgs.go b/release/dist/unixpkgs/pkgs.go index 61a6235df..f8a32fc3d 100644 --- a/release/dist/unixpkgs/pkgs.go +++ b/release/dist/unixpkgs/pkgs.go @@ -14,7 +14,6 @@ import ( "os" "path/filepath" "strings" - "time" "github.com/goreleaser/nfpm" "tailscale.com/release/dist" @@ -71,7 +70,6 @@ func (t *tgzTarget) Build(b *dist.Build) ([]string, error) { tw := tar.NewWriter(gw) defer tw.Close() - buildTime := time.Now() addFile := func(src, dst string, mode int64) error { f, err := os.Open(src) if err != nil { @@ -86,7 +84,7 @@ func (t *tgzTarget) Build(b *dist.Build) ([]string, error) { Name: dst, Size: fi.Size(), Mode: mode, - ModTime: buildTime, + ModTime: b.Time, Uid: 0, Gid: 0, Uname: "root", @@ -104,7 +102,7 @@ func (t *tgzTarget) Build(b *dist.Build) ([]string, error) { hdr := &tar.Header{ Name: name + "/", Mode: 0755, - ModTime: buildTime, + ModTime: b.Time, Uid: 0, Gid: 0, Uname: "root", diff --git a/tool/gocross/autoflags.go b/tool/gocross/autoflags.go index 0fd46c4fa..f51d20671 100644 --- a/tool/gocross/autoflags.go +++ b/tool/gocross/autoflags.go @@ -153,7 +153,9 @@ func autoflagsForTest(argv []string, env *Environment, goroot, nativeGOOS, nativ env.Set("GOOS", targetOS) env.Set("GOARCH", targetArch) - env.Set("GOARM", "5") // TODO: fix, see go/internal-bug/3092 + if !env.IsSet("GOARM") { + env.Set("GOARM", "5") // TODO: fix, see go/internal-bug/3092 + } env.Set("GOMIPS", "softfloat") env.Set("CGO_ENABLED", boolStr(cgo)) env.Set("CGO_CFLAGS", strings.Join(cgoCflags, " ")) diff --git a/version/mkversion/mkversion.go b/version/mkversion/mkversion.go index af5244e91..a7d60f528 100644 --- a/version/mkversion/mkversion.go +++ b/version/mkversion/mkversion.go @@ -61,6 +61,10 @@ type VersionInfo struct { // Winres is the version string that gets embedded into Windows exe // metadata. It is of the form "x,y,z,0". Winres string + // Synology is a map of Synology DSM major version to the + // Tailscale numeric version that gets embedded in Synology spk + // files. + Synology map[int]int64 // GitDate is the unix timestamp of GitHash's commit date. GitDate string // OtherDate is the unix timestamp of OtherHash's commit date, if any. @@ -239,6 +243,10 @@ func mkOutput(v verInfo) (VersionInfo, error) { GitHash: fmt.Sprintf("%s", v.hash), GitDate: fmt.Sprintf("%s", v.date), Track: track, + Synology: map[int]int64{ + 6: 6*1_000_000_000 + int64(v.major-1)*1_000_000 + int64(v.minor)*1_000 + int64(v.patch), + 7: 7*1_000_000_000 + int64(v.major-1)*1_000_000 + int64(v.minor)*1_000 + int64(v.patch), + }, } if v.otherHash != "" {