release/dist/qnap: add qnap target builder

Creates new QNAP builder target, which builds go binaries then uses
docker to build into QNAP packages. Much of the docker/script code
here is pulled over from https://github.com/tailscale/tailscale-qpkg,
with adaptation into our builder structures.

The qnap/Tailscale folder contains static resources needed to build
Tailscale qpkg packages, and is an exact copy of the existing folder
in the tailscale-qpkg repo.

Builds can be run with:
```
sudo ./tool/go run ./cmd/dist build qnap
```

Updates tailscale/tailscale-qpkg#135

Signed-off-by: Sonia Appasamy <sonia@tailscale.com>
Sonia Appasamy 2 weeks ago
parent 3ef7f895c8
commit 047870c9c0
No known key found for this signature in database

2
cmd/dist/dist.go vendored

@ -13,6 +13,7 @@ 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"
)
@ -37,6 +38,7 @@ 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)...)
ret = append(ret, qnap.Targets(nil)...)
return ret, nil
}

@ -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,21 @@ 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))
if b.onCloseFuncs != nil {
for _, f := range b.onCloseFuncs {
errs = append(errs, f())
}
}
return errors.Join(errs...)
}
// Build builds all targets concurrently.

@ -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}"

@ -0,0 +1 @@
,/Tailscale.sh,
1 /Tailscale.sh

Binary file not shown.

After

Width:  |  Height:  |  Size: 600 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 741 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

@ -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(){
#}

@ -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"

@ -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

@ -0,0 +1,2 @@
Options +ExecCGI
AddHandler cgi-script .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/"

@ -0,0 +1,14 @@
#!/bin/bash
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
# Clean up folders and files created for build.
rm -r /Tailscale/$ARCH
rm /Tailscale/sed*
rm /Tailscale/qpkg.cfg

@ -0,0 +1,188 @@
// 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 dist.Signer
}
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)
}
qnapBuilds := getQnapBuilds(b)
inner, err := qnapBuilds.buildInnerPackage(b, t.goenv)
if err != nil {
return nil, err
}
return t.buildQPKG(b, qnapBuilds, inner)
}
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)
}
qnapTag := "1" // currently static, we don't seem to bump this
filename := fmt.Sprintf("Tailscale_%s-%s_%s.qpkg", b.Version.Short, qnapTag, t.arch)
out := filepath.Join(b.Out, filename)
log.Printf("Building %s", out)
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.tsPath),
"-v", fmt.Sprintf("%s:/tailscaled", inner.tsdPath),
// 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()
if err := cmd.Run(); err != nil {
return nil, fmt.Errorf("docker run: %w", err)
}
// TODO(sonia): Use signer?
return []string{out, out + ".md5"}, nil
}
type qnapBuildsMemoizeKey struct{}
type innerPkg struct {
tsPath string // tailscale binary path
tsdPath string // tailscaled binary path
}
// qnapBuilds is extra build context shared by all qnap builds.
type qnapBuilds struct {
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{tsPath: tsPath, tsdPath: tsdPath}, nil
})
}
func (m *qnapBuilds) makeDockerImage(b *dist.Build) error {
return b.Once("make-qnap-docker-image", func() error {
out := "build.tailscale.io/qdk:latest"
var exitErr *exec.ExitError
cmd := b.Command(b.Repo, "docker", "image", "inspect", out)
if err := cmd.Run(); err == nil {
return nil // image already exists, return early
} else if !errors.As(err, &exitErr) {
return fmt.Errorf("docker image inspect: %w", err)
}
log.Printf("Building qnapbuilder docker image")
cmd = b.Command(b.Repo, "docker", "build",
"-f", filepath.Join(b.Repo, "release/dist/qnap/Dockerfile.qpkg"),
"-t", out,
filepath.Join(b.Repo, "release/dist/qnap/"),
)
if err := cmd.Run(); err != nil {
return fmt.Errorf("docker build: %w", err)
}
return nil
})
}

@ -0,0 +1,67 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
package qnap
import "tailscale.com/release/dist"
func Targets(signer dist.Signer) []dist.Target {
return []dist.Target{
&target{
arch: "x86",
goenv: map[string]string{
"GOOS": "linux",
"GOARCH": "386",
},
signer: signer,
},
&target{
arch: "x86_ce53xx",
goenv: map[string]string{
"GOOS": "linux",
"GOARCH": "386",
},
signer: signer,
},
&target{
arch: "x86_64",
goenv: map[string]string{
"GOOS": "linux",
"GOARCH": "amd64",
},
signer: signer,
},
&target{
arch: "arm-x31",
goenv: map[string]string{
"GOOS": "linux",
"GOARCH": "arm",
},
signer: signer,
},
&target{
arch: "arm-x41",
goenv: map[string]string{
"GOOS": "linux",
"GOARCH": "arm",
},
signer: signer,
},
&target{
arch: "arm-x19",
goenv: map[string]string{
"GOOS": "linux",
"GOARCH": "arm",
},
signer: signer,
},
&target{
arch: "arm_64",
goenv: map[string]string{
"GOOS": "linux",
"GOARCH": "arm64",
},
signer: signer,
},
}
}
Loading…
Cancel
Save