diff --git a/cmd/dist/dist.go b/cmd/dist/dist.go index 1d57de98d..05f5bbfb2 100644 --- a/cmd/dist/dist.go +++ b/cmd/dist/dist.go @@ -13,11 +13,16 @@ import ( "tailscale.com/release/dist" "tailscale.com/release/dist/cli" + "tailscale.com/release/dist/qnap" "tailscale.com/release/dist/synology" "tailscale.com/release/dist/unixpkgs" ) -var synologyPackageCenter bool +var ( + synologyPackageCenter bool + qnapPrivateKeyPath string + qnapCertificatePath string +) func getTargets() ([]dist.Target, error) { var ret []dist.Target @@ -37,6 +42,10 @@ func getTargets() ([]dist.Target, error) { // To build for package center, run // ./tool/go run ./cmd/dist build --synology-package-center synology ret = append(ret, synology.Targets(synologyPackageCenter, nil)...) + if (qnapPrivateKeyPath == "") != (qnapCertificatePath == "") { + return nil, errors.New("both --qnap-private-key-path and --qnap-certificate-path must be set") + } + ret = append(ret, qnap.Targets(qnapPrivateKeyPath, qnapCertificatePath)...) return ret, nil } @@ -45,6 +54,8 @@ func main() { 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") + subcmd.FlagSet.StringVar(&qnapPrivateKeyPath, "qnap-private-key-path", "", "sign qnap packages with given key (must also provide --qnap-certificate-path)") + subcmd.FlagSet.StringVar(&qnapCertificatePath, "qnap-certificate-path", "", "sign qnap packages with given certificate (must also provide --qnap-private-key-path)") } } diff --git a/release/dist/dist.go b/release/dist/dist.go index cd3d1b044..802d9041b 100644 --- a/release/dist/dist.go +++ b/release/dist/dist.go @@ -88,6 +88,8 @@ type Build struct { // number of CPU cores, which empirically keeps the builder responsive // without impacting overall build time. goBuildLimit chan struct{} + + onCloseFuncs []func() error // funcs to be called when Builder is closed } // NewBuild creates a new Build rooted at repo, and writing artifacts to out. @@ -126,9 +128,19 @@ func NewBuild(repo, out string) (*Build, error) { return b, nil } -// Close ends the build and cleans up temporary files. +func (b *Build) AddOnCloseFunc(f func() error) { + b.onCloseFuncs = append(b.onCloseFuncs, f) +} + +// Close ends the build, cleans up temporary files, +// and runs any onCloseFuncs. func (b *Build) Close() error { - return os.RemoveAll(b.Tmp) + var errs []error + errs = append(errs, os.RemoveAll(b.Tmp)) + for _, f := range b.onCloseFuncs { + errs = append(errs, f()) + } + return errors.Join(errs...) } // Build builds all targets concurrently. diff --git a/release/dist/qnap/Dockerfile.qpkg b/release/dist/qnap/Dockerfile.qpkg new file mode 100644 index 000000000..135d5d20f --- /dev/null +++ b/release/dist/qnap/Dockerfile.qpkg @@ -0,0 +1,9 @@ +FROM ubuntu:20.04 + +RUN apt-get update -y && \ + apt-get install -y --no-install-recommends \ + git-core \ + ca-certificates +RUN git clone https://github.com/qnap-dev/QDK.git +RUN cd /QDK && ./InstallToUbuntu.sh install +ENV PATH="/usr/share/QDK/bin:${PATH}" \ No newline at end of file diff --git a/release/dist/qnap/Tailscale/build_sign.csv b/release/dist/qnap/Tailscale/build_sign.csv new file mode 100755 index 000000000..430183ab7 --- /dev/null +++ b/release/dist/qnap/Tailscale/build_sign.csv @@ -0,0 +1 @@ +,/Tailscale.sh, diff --git a/release/dist/qnap/Tailscale/config/.gitkeep b/release/dist/qnap/Tailscale/config/.gitkeep new file mode 100755 index 000000000..e69de29bb diff --git a/release/dist/qnap/Tailscale/icons/.gitkeep b/release/dist/qnap/Tailscale/icons/.gitkeep new file mode 100755 index 000000000..e69de29bb diff --git a/release/dist/qnap/Tailscale/icons/Tailscale.gif b/release/dist/qnap/Tailscale/icons/Tailscale.gif new file mode 100644 index 000000000..7ba203c33 Binary files /dev/null and b/release/dist/qnap/Tailscale/icons/Tailscale.gif differ diff --git a/release/dist/qnap/Tailscale/icons/Tailscale_80.gif b/release/dist/qnap/Tailscale/icons/Tailscale_80.gif new file mode 100644 index 000000000..38e797bc2 Binary files /dev/null and b/release/dist/qnap/Tailscale/icons/Tailscale_80.gif differ diff --git a/release/dist/qnap/Tailscale/icons/Tailscale_gray.gif b/release/dist/qnap/Tailscale/icons/Tailscale_gray.gif new file mode 100644 index 000000000..26fd5130d Binary files /dev/null and b/release/dist/qnap/Tailscale/icons/Tailscale_gray.gif differ diff --git a/release/dist/qnap/Tailscale/package_routines b/release/dist/qnap/Tailscale/package_routines new file mode 100755 index 000000000..faa32f32a --- /dev/null +++ b/release/dist/qnap/Tailscale/package_routines @@ -0,0 +1,143 @@ +###################################################################### +# List of available definitions (it's not necessary to uncomment them) +###################################################################### +###### Command definitions ##### +#CMD_AWK="/bin/awk" +#CMD_CAT="/bin/cat" +#CMD_CHMOD="/bin/chmod" +#CMD_CHOWN="/bin/chown" +#CMD_CP="/bin/cp" +#CMD_CUT="/bin/cut" +#CMD_DATE="/bin/date" +#CMD_ECHO="/bin/echo" +#CMD_EXPR="/usr/bin/expr" +#CMD_FIND="/usr/bin/find" +#CMD_GETCFG="/sbin/getcfg" +#CMD_GREP="/bin/grep" +#CMD_GZIP="/bin/gzip" +#CMD_HOSTNAME="/bin/hostname" +#CMD_LN="/bin/ln" +#CMD_LOG_TOOL="/sbin/log_tool" +#CMD_MD5SUM="/bin/md5sum" +#CMD_MKDIR="/bin/mkdir" +#CMD_MV="/bin/mv" +#CMD_RM="/bin/rm" +#CMD_RMDIR="/bin/rmdir" +#CMD_SED="/bin/sed" +#CMD_SETCFG="/sbin/setcfg" +#CMD_SLEEP="/bin/sleep" +#CMD_SORT="/usr/bin/sort" +#CMD_SYNC="/bin/sync" +#CMD_TAR="/bin/tar" +#CMD_TOUCH="/bin/touch" +#CMD_WGET="/usr/bin/wget" +#CMD_WLOG="/sbin/write_log" +#CMD_XARGS="/usr/bin/xargs" +#CMD_7Z="/usr/local/sbin/7z" +# +###### System definitions ##### +#SYS_EXTRACT_DIR="$(pwd)" +#SYS_CONFIG_DIR="/etc/config" +#SYS_INIT_DIR="/etc/init.d" +#SYS_STARTUP_DIR="/etc/rcS.d" +#SYS_SHUTDOWN_DIR="/etc/rcK.d" +#SYS_RSS_IMG_DIR="/home/httpd/RSS/images" +#SYS_QPKG_DATA_FILE_GZIP="./data.tar.gz" +#SYS_QPKG_DATA_FILE_BZIP2="./data.tar.bz2" +#SYS_QPKG_DATA_FILE_7ZIP="./data.tar.7z" +#SYS_QPKG_DATA_CONFIG_FILE="./conf.tar.gz" +#SYS_QPKG_DATA_MD5SUM_FILE="./md5sum" +#SYS_QPKG_DATA_PACKAGES_FILE="./Packages.gz" +#SYS_QPKG_CONFIG_FILE="$SYS_CONFIG_DIR/qpkg.conf" +#SYS_QPKG_CONF_FIELD_QPKGFILE="QPKG_File" +#SYS_QPKG_CONF_FIELD_NAME="Name" +#SYS_QPKG_CONF_FIELD_VERSION="Version" +#SYS_QPKG_CONF_FIELD_ENABLE="Enable" +#SYS_QPKG_CONF_FIELD_DATE="Date" +#SYS_QPKG_CONF_FIELD_SHELL="Shell" +#SYS_QPKG_CONF_FIELD_INSTALL_PATH="Install_Path" +#SYS_QPKG_CONF_FIELD_CONFIG_PATH="Config_Path" +#SYS_QPKG_CONF_FIELD_WEBUI="WebUI" +#SYS_QPKG_CONF_FIELD_WEBPORT="Web_Port" +#SYS_QPKG_CONF_FIELD_SERVICEPORT="Service_Port" +#SYS_QPKG_CONF_FIELD_SERVICE_PIDFILE="Pid_File" +#SYS_QPKG_CONF_FIELD_AUTHOR="Author" +#SYS_QPKG_CONF_FIELD_RC_NUMBER="RC_Number" +## The following variables are assigned values at run-time. +#SYS_HOSTNAME=$($CMD_HOSTNAME) +## Data file name (one of SYS_QPKG_DATA_FILE_GZIP, SYS_QPKG_DATA_FILE_BZIP2, +## or SYS_QPKG_DATA_FILE_7ZIP) +#SYS_QPKG_DATA_FILE= +## Base location. +#SYS_QPKG_BASE="" +## Base location of QPKG installed packages. +#SYS_QPKG_INSTALL_PATH="" +## Location of installed software. +#SYS_QPKG_DIR="" +## If the QPKG should be enabled or disabled after the installation/upgrade. +#SYS_QPKG_SERVICE_ENABLED="" +## Architecture of the device the QPKG is installed on. +#SYS_CPU_ARCH="" +## Name and location of system shares +#SYS_PUBLIC_SHARE="" +#SYS_PUBLIC_PATH="" +#SYS_DOWNLOAD_SHARE="" +#SYS_DOWNLOAD_PATH="" +#SYS_MULTIMEDIA_SHARE="" +#SYS_MULTIMEDIA_PATH="" +#SYS_RECORDINGS_SHARE="" +#SYS_RECORDINGS_PATH="" +#SYS_USB_SHARE="" +#SYS_USB_PATH="" +#SYS_WEB_SHARE="" +#SYS_WEB_PATH="" +## Path to ipkg or opkg package tool if installed. +#CMD_PKG_TOOL= +# +###################################################################### +# All package specific functions shall call 'err_log MSG' if an error +# is detected that shall terminate the installation. +###################################################################### +# +###################################################################### +# Define any package specific operations that shall be performed when +# the package is removed. +###################################################################### +#PKG_PRE_REMOVE="{ +#}" +# +#PKG_MAIN_REMOVE="{ +#}" +# +PKG_POST_REMOVE="{ + rm -f /home/httpd/cgi-bin/qpkg/Tailscale + rm -rf /tmp/tailscale + if [ -f /etc/resolv.pre-tailscale-backup.conf ] && grep -q 100.100.100.100 /etc/resolv.conf; then + mv /etc/resolv.pre-tailscale-backup.conf /etc/resolv.conf + fi +}" +# +###################################################################### +# Define any package specific initialization that shall be performed +# before the package is installed. +###################################################################### +#pkg_init(){ +#} +# +###################################################################### +# Define any package specific requirement checks that shall be +# performed before the package is installed. +###################################################################### +#pkg_check_requirement(){ +#} +# +###################################################################### +# Define any package specific operations that shall be performed when +# the package is installed. +###################################################################### +pkg_install(){ + ${CMD_MKDIR} -p ${SYS_QPKG_DIR}/state +} + +#pkg_post_install(){ +#} diff --git a/release/dist/qnap/Tailscale/qpkg.cfg.in b/release/dist/qnap/Tailscale/qpkg.cfg.in new file mode 100644 index 000000000..4084eb2ca --- /dev/null +++ b/release/dist/qnap/Tailscale/qpkg.cfg.in @@ -0,0 +1,99 @@ +# Name of the packaged application. +QPKG_NAME="Tailscale" +# Name of the display application. +#QPKG_DISPLAY_NAME="" +# Version of the packaged application. +QPKG_VER="$QPKG_VER" +# Author or maintainer of the package +QPKG_AUTHOR="Tailscale Inc." +# License for the packaged application +#QPKG_LICENSE="" +# One-line description of the packaged application +#QPKG_SUMMARY="Connect all your devices using WireGuard, without the hassle." + +# Preferred number in start/stop sequence. +QPKG_RC_NUM="101" +# Init-script used to control the start and stop of the installed application. +QPKG_SERVICE_PROGRAM="Tailscale.sh" + +# Optional 1 is enable. Path of starting/ stopping shall script. (no start/stop on App Center) +#QPKG_DISABLE_APPCENTER_UI_SERVICE=1 + +# Specifies any packages required for the current package to operate. +QPKG_REQUIRE="" +# Specifies what packages cannot be installed if the current package +# is to operate properly. +#QPKG_CONFLICT="Python" +# Name of configuration file (multiple definitions are allowed). +#QPKG_CONFIG="Tailscale.cfg" +#QPKG_CONFIG="/etc/config/myApp.conf" +# Port number used by service program. +QPKG_SERVICE_PORT="41641" +# Location of file with running service's PID +#QPKG_SERVICE_PIDFILE="" +# Relative path to web interface +QPKG_WEBUI="/cgi-bin/qpkg/Tailscale/index.cgi" +# Port number for the web interface. +#QPKG_WEB_PORT="" +# Port number for the SSL web interface. +#QPKG_WEB_SSL_PORT="" + +# Use QTS HTTP Proxy and set Proxy_Path in the qpkg.conf. +# When the QPKG has its own HTTP service port, and want clients to connect via QTS HTTP port (default 8080). +# Usually use this option when the QPKG need to connect via myQNAPcloud service. +QPKG_USE_PROXY="1" +#QPKG_PROXY_PATH="/qpkg_name" + +#Desktop Application (since 4.1) +# Set value to 1 means to open the QPKG's Web UI inside QTS desktop instead of new window. +#QPKG_DESKTOP_APP="1" +# Desktop Application Window default inner width (since 4.1) (not over 1178) +#QPKG_DESKTOP_APP_WIN_WIDTH="" +# Desktop Application Window default inner height (since 4.1) (not over 600) +#QPKG_DESKTOP_APP_WIN_HEIGHT="" + +# Minimum QTS version requirement +QTS_MINI_VERSION="5.0.0" +# Maximum QTS version requirement +#QTS_MAX_VERSION="5.0.0" + +# Select volume +# 1: support installation +# 2: support migration +# 3 (1+2): support both installation and migration +QPKG_VOLUME_SELECT="1" + +# Set timeout for QPKG enable and QPKG disable (since 4.1.0) +# Format in seconds (enable, disable) +#QPKG_TIMEOUT="10,30" + +# Visible setting for the QPKG that has web UI, show this QPKG on the Main menu of +# 1(default): administrators, 2: all NAS users. +QPKG_VISIBLE="1" + +# Location of icons for the packaged application. +QDK_DATA_DIR_ICONS="icons" +# Location of files specific to arm-x19 packages. +#QDK_DATA_DIR_X19="arm-x19" +# Location of files specific to arm-x31 packages. +#QDK_DATA_DIR_X31="arm-x31" +# Location of files specific to arm-x41 packages. +#QDK_DATA_DIR_X41="arm_al" +# Location of files specific to x86 packages. +#QDK_DATA_DIR_X86="x86" +# Location of files specific to x86 (64-bit) packages. +#QDK_DATA_DIR_X86_64="x86_64" +# Location of files common to all architectures. +QDK_DATA_DIR_SHARED="shared" +# Location of configuration files. +#QDK_DATA_DIR_CONFIG="config" +# Name of local data package. +#QDK_DATA_FILE="" +# Name of extra package (multiple definitions are allowed). +#QDK_EXTRA_FILE="" +# For QNAP code signing (currently can be done only inside QNAP) +# Uncomment the following four options if you want to enable code signing for this QPKG +#QNAP_CODE_SIGNING="0" +#QNAP_CODE_SIGNING_SERVER_IP="codesigning.qnap.com.tw" +#QNAP_CODE_SIGNING_SERVER_PORT="5001" +#QNAP_CODE_SIGNING_CSV="build_sign.csv" \ No newline at end of file diff --git a/release/dist/qnap/Tailscale/shared/Tailscale.sh b/release/dist/qnap/Tailscale/shared/Tailscale.sh new file mode 100755 index 000000000..78b3a45d2 --- /dev/null +++ b/release/dist/qnap/Tailscale/shared/Tailscale.sh @@ -0,0 +1,50 @@ +#!/bin/sh +CONF=/etc/config/qpkg.conf +QPKG_NAME="Tailscale" +QPKG_ROOT=`/sbin/getcfg ${QPKG_NAME} Install_Path -f ${CONF}` +QPKG_PORT=`/sbin/getcfg ${QPKG_NAME} Service_Port -f ${CONF}` +export QNAP_QPKG=${QPKG_NAME} +set -e + +case "$1" in + start) + ENABLED=$(/sbin/getcfg ${QPKG_NAME} Enable -u -d FALSE -f ${CONF}) + if [ "${ENABLED}" != "TRUE" ]; then + echo "${QPKG_NAME} is disabled." + exit 1 + fi + mkdir -p /home/httpd/cgi-bin/qpkg + ln -sf ${QPKG_ROOT}/ui /home/httpd/cgi-bin/qpkg/${QPKG_NAME} + mkdir -p -m 0755 /tmp/tailscale + if [ -e /tmp/tailscale/tailscaled.pid ]; then + PID=$(cat /tmp/tailscale/tailscaled.pid) + if [ -d /proc/${PID}/ ]; then + echo "${QPKG_NAME} is already running." + exit 0 + fi + fi + ${QPKG_ROOT}/tailscaled --port ${QPKG_PORT} --statedir=${QPKG_ROOT}/state --socket=/tmp/tailscale/tailscaled.sock 2> /dev/null & + echo $! > /tmp/tailscale/tailscaled.pid + ;; + + stop) + if [ -e /tmp/tailscale/tailscaled.pid ]; then + PID=$(cat /tmp/tailscale/tailscaled.pid) + kill -9 ${PID} || true + rm -f /tmp/tailscale/tailscaled.pid + fi + ;; + + restart) + $0 stop + $0 start + ;; + remove) + ;; + + *) + echo "Usage: $0 {start|stop|restart|remove}" + exit 1 +esac + +exit 0 diff --git a/release/dist/qnap/Tailscale/shared/ui/.htaccess b/release/dist/qnap/Tailscale/shared/ui/.htaccess new file mode 100644 index 000000000..1695f503f --- /dev/null +++ b/release/dist/qnap/Tailscale/shared/ui/.htaccess @@ -0,0 +1,2 @@ +Options +ExecCGI +AddHandler cgi-script .cgi diff --git a/release/dist/qnap/Tailscale/shared/ui/index.cgi b/release/dist/qnap/Tailscale/shared/ui/index.cgi new file mode 100755 index 000000000..961fc8bc4 --- /dev/null +++ b/release/dist/qnap/Tailscale/shared/ui/index.cgi @@ -0,0 +1,5 @@ +#!/bin/sh +CONF=/etc/config/qpkg.conf +QPKG_NAME="Tailscale" +QPKG_ROOT=$(/sbin/getcfg ${QPKG_NAME} Install_Path -f ${CONF} -d"") +exec "${QPKG_ROOT}/tailscale" --socket=/tmp/tailscale/tailscaled.sock web --cgi --prefix="/cgi-bin/qpkg/Tailscale/index.cgi/" diff --git a/release/dist/qnap/build-qpkg.sh b/release/dist/qnap/build-qpkg.sh new file mode 100755 index 000000000..59e214472 --- /dev/null +++ b/release/dist/qnap/build-qpkg.sh @@ -0,0 +1,19 @@ +#!/bin/bash + +set -eu + +# Clean up folders and files created during build. +function cleanup() { + rm -rf /Tailscale/$ARCH + rm -f /Tailscale/sed* + rm -f /Tailscale/qpkg.cfg +} +trap cleanup EXIT + +mkdir -p /Tailscale/$ARCH +cp /tailscaled /Tailscale/$ARCH/tailscaled +cp /tailscale /Tailscale/$ARCH/tailscale + +sed "s/\$QPKG_VER/$TSTAG-$QNAPTAG/g" /Tailscale/qpkg.cfg.in > /Tailscale/qpkg.cfg + +qbuild --root /Tailscale --build-arch $ARCH --build-dir /out diff --git a/release/dist/qnap/pkgs.go b/release/dist/qnap/pkgs.go new file mode 100644 index 000000000..5ee29e988 --- /dev/null +++ b/release/dist/qnap/pkgs.go @@ -0,0 +1,229 @@ +// Copyright (c) Tailscale Inc & AUTHORS +// SPDX-License-Identifier: BSD-3-Clause + +// Package qnap contains dist Targets for building QNAP Tailscale packages. +// +// QNAP dev docs over at https://www.qnap.com/en/how-to/tutorial/article/qpkg-development-guidelines. +package qnap + +import ( + "errors" + "fmt" + "log" + "os" + "os/exec" + "path/filepath" + "sync" + + "tailscale.com/release/dist" +) + +type target struct { + goenv map[string]string + arch string + signer *signer +} + +type signer struct { + privateKeyPath string + certificatePath string +} + +func (t *target) String() string { + return fmt.Sprintf("qnap/%s", t.arch) +} + +func (t *target) Build(b *dist.Build) ([]string, error) { + // Stop early if we don't have docker running. + if _, err := exec.LookPath("docker"); err != nil { + return nil, fmt.Errorf("docker not found, cannot build: %w", err) + } + + if t.signer != nil { + if err := t.setUpSignatureFiles(b); err != nil { + return nil, err + } + } + + qnapBuilds := getQnapBuilds(b) + inner, err := qnapBuilds.buildInnerPackage(b, t.goenv) + if err != nil { + return nil, err + } + + return t.buildQPKG(b, qnapBuilds, inner) +} + +const ( + qnapTag = "1" // currently static, we don't seem to bump this +) + +func (t *target) buildQPKG(b *dist.Build, qnapBuilds *qnapBuilds, inner *innerPkg) ([]string, error) { + if _, err := exec.LookPath("docker"); err != nil { + return nil, fmt.Errorf("docker not found, cannot build: %w", err) + } + + if err := qnapBuilds.makeDockerImage(b); err != nil { + return nil, fmt.Errorf("makeDockerImage: %w", err) + } + + filename := fmt.Sprintf("Tailscale_%s-%s_%s.qpkg", b.Version.Short, qnapTag, t.arch) + filePath := filepath.Join(b.Out, filename) + + cmd := b.Command(b.Repo, "docker", "run", "--rm", + "-e", fmt.Sprintf("ARCH=%s", t.arch), + "-e", fmt.Sprintf("TSTAG=%s", b.Version.Short), + "-e", fmt.Sprintf("QNAPTAG=%s", qnapTag), + "-v", fmt.Sprintf("%s:/tailscale", inner.tailscalePath), + "-v", fmt.Sprintf("%s:/tailscaled", inner.tailscaledPath), + // Tailscale folder has QNAP package setup files needed for building. + "-v", fmt.Sprintf("%s:/Tailscale", filepath.Join(b.Repo, "release/dist/qnap/Tailscale")), + "-v", fmt.Sprintf("%s:/build-qpkg.sh", filepath.Join(b.Repo, "release/dist/qnap/build-qpkg.sh")), + "-v", fmt.Sprintf("%s:/out", b.Out), + "build.tailscale.io/qdk:latest", + "/build-qpkg.sh", + ) + + // dist.Build runs target builds in parallel goroutines by default. + // For QNAP, this is an issue because the underlaying qbuild builder will + // create tmp directories in the shared docker image that end up conflicting + // with one another. + // So we use a mutex to only allow one "docker run" at a time. + qnapBuilds.dockerImageMu.Lock() + defer qnapBuilds.dockerImageMu.Unlock() + + log.Printf("Building %s", filePath) + out, err := cmd.CombinedOutput() + if err != nil { + return nil, fmt.Errorf("docker run %v: %s", err, out) + } + + return []string{filePath, filePath + ".md5"}, nil +} + +func (t *target) setUpSignatureFiles(b *dist.Build) error { + return b.Once(fmt.Sprintf("qnap-signature-%s-%s", t.signer.privateKeyPath, t.signer.certificatePath), func() error { + log.Print("Setting up qnap signature files") + + key, err := os.ReadFile(t.signer.privateKeyPath) + if err != nil { + return err + } + cert, err := os.ReadFile(t.signer.certificatePath) + if err != nil { + return err + } + + // QNAP's qbuild command expects key and cert files to be in the root + // of the project directory (in our case release/dist/qnap/Tailscale). + // So here, we copy the key and cert over to the project folder for the + // duration of qnap package building and then delete them on close. + + keyPath := filepath.Join(b.Repo, "release/dist/qnap/Tailscale/private_key") + if err := os.WriteFile(keyPath, key, 0400); err != nil { + return err + } + certPath := filepath.Join(b.Repo, "release/dist/qnap/Tailscale/certificate") + if err := os.WriteFile(certPath, cert, 0400); err != nil { + return err + } + + b.AddOnCloseFunc(func() error { + return errors.Join(os.Remove(keyPath), os.Remove(certPath)) + }) + return nil + }) +} + +type qnapBuildsMemoizeKey struct{} + +type innerPkg struct { + tailscalePath string + tailscaledPath string +} + +// qnapBuilds holds extra build context shared by all qnap builds. +type qnapBuilds struct { + // innerPkgs contains per-goenv compiled binary paths. + // It is used to avoid repeated compilations for the same architecture. + innerPkgs dist.Memoize[*innerPkg] + dockerImageMu sync.Mutex +} + +// getQnapBuilds returns the qnapBuilds for b, creating one if needed. +func getQnapBuilds(b *dist.Build) *qnapBuilds { + return b.Extra(qnapBuildsMemoizeKey{}, func() any { return new(qnapBuilds) }).(*qnapBuilds) +} + +// buildInnerPackage builds the go binaries used for qnap packages. +// These binaries get embedded with Tailscale package metadata to form qnap +// releases. +func (m *qnapBuilds) buildInnerPackage(b *dist.Build, goenv map[string]string) (*innerPkg, error) { + return m.innerPkgs.Do(goenv, 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 + } + + // The go binaries above get built and put into a /tmp directory created + // by b.TmpDir(). But, we build QNAP with docker, which doesn't always + // allow for mounting tmp directories (seemingly dependent on docker + // host). + // https://stackoverflow.com/questions/65267251/docker-bind-mount-directory-in-tmp-not-working + // + // So here, we move the binaries into a directory within the b.Repo + // path and clean it up when the builder closes. + + tmpDir := filepath.Join(b.Repo, fmt.Sprintf("/tmp-qnap-%s-%s-%s", b.Version.Short, goenv["GOOS"], goenv["GOARCH"])) + if err = os.MkdirAll(tmpDir, 0755); err != nil { + return nil, err + } + b.AddOnCloseFunc(func() error { + return os.RemoveAll(tmpDir) + }) + + tsBytes, err := os.ReadFile(ts) + if err != nil { + return nil, err + } + tsdBytes, err := os.ReadFile(tsd) + if err != nil { + return nil, err + } + + tsPath := filepath.Join(tmpDir, "tailscale") + if err := os.WriteFile(tsPath, tsBytes, 0755); err != nil { + return nil, err + } + tsdPath := filepath.Join(tmpDir, "tailscaled") + if err := os.WriteFile(tsdPath, tsdBytes, 0755); err != nil { + return nil, err + } + + return &innerPkg{tailscalePath: tsPath, tailscaledPath: tsdPath}, nil + }) +} + +func (m *qnapBuilds) makeDockerImage(b *dist.Build) error { + return b.Once("make-qnap-docker-image", func() error { + log.Printf("Building qnapbuilder docker image") + + cmd := b.Command(b.Repo, "docker", "build", + "-f", filepath.Join(b.Repo, "release/dist/qnap/Dockerfile.qpkg"), + "-t", "build.tailscale.io/qdk:latest", + filepath.Join(b.Repo, "release/dist/qnap/"), + ) + out, err := cmd.CombinedOutput() + if err != nil { + return fmt.Errorf("docker build %v: %s", err, out) + } + return nil + }) +} diff --git a/release/dist/qnap/targets.go b/release/dist/qnap/targets.go new file mode 100644 index 000000000..a069dd623 --- /dev/null +++ b/release/dist/qnap/targets.go @@ -0,0 +1,75 @@ +// Copyright (c) Tailscale Inc & AUTHORS +// SPDX-License-Identifier: BSD-3-Clause + +package qnap + +import "tailscale.com/release/dist" + +// Targets defines the dist.Targets for QNAP devices. +// +// If privateKeyPath and certificatePath are both provided non-empty, +// these targets will be signed for QNAP app store release with built. +func Targets(privateKeyPath, certificatePath string) []dist.Target { + var signerInfo *signer + if privateKeyPath != "" && certificatePath != "" { + signerInfo = &signer{privateKeyPath, certificatePath} + } + return []dist.Target{ + &target{ + arch: "x86", + goenv: map[string]string{ + "GOOS": "linux", + "GOARCH": "386", + }, + signer: signerInfo, + }, + &target{ + arch: "x86_ce53xx", + goenv: map[string]string{ + "GOOS": "linux", + "GOARCH": "386", + }, + signer: signerInfo, + }, + &target{ + arch: "x86_64", + goenv: map[string]string{ + "GOOS": "linux", + "GOARCH": "amd64", + }, + signer: signerInfo, + }, + &target{ + arch: "arm-x31", + goenv: map[string]string{ + "GOOS": "linux", + "GOARCH": "arm", + }, + signer: signerInfo, + }, + &target{ + arch: "arm-x41", + goenv: map[string]string{ + "GOOS": "linux", + "GOARCH": "arm", + }, + signer: signerInfo, + }, + &target{ + arch: "arm-x19", + goenv: map[string]string{ + "GOOS": "linux", + "GOARCH": "arm", + }, + signer: signerInfo, + }, + &target{ + arch: "arm_64", + goenv: map[string]string{ + "GOOS": "linux", + "GOARCH": "arm64", + }, + signer: signerInfo, + }, + } +}