diff --git a/.github/workflows/pipeline.yml b/.github/workflows/pipeline.yml index 0684986e7..98dd00a0b 100644 --- a/.github/workflows/pipeline.yml +++ b/.github/workflows/pipeline.yml @@ -332,7 +332,6 @@ jobs: sudo GIT_TAG=$GIT_TAG release/wix/build_msi.sh ${GITHUB_WORKSPACE} amd64 du -h binaries/msi/*.msi - - name: Upload MSI files uses: actions/upload-artifact@v4 with: @@ -341,11 +340,16 @@ jobs: retention-days: 7 release: - name: Release - needs: [build, msi, push-manifest] + name: Package/Release + needs: [build, msi] runs-on: ubuntu-latest + outputs: + package_list: ${{ steps.set-package-list.outputs.package_list }} steps: - uses: actions/checkout@v4 + with: + fetch-depth: 0 + fetch-tags: true - uses: actions/download-artifact@v4 with: @@ -366,3 +370,56 @@ jobs: args: "release --clean -f release/goreleaser.yml ${{ env.RELEASE_FLAGS }}" env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Remove build artifacts + run: | + ls -l ./dist + rm ./dist/*.tar.gz ./dist/*.zip + + - name: Upload all-packages artifact + uses: actions/upload-artifact@v4 + with: + name: packages + path: dist/navidrome_v* + + - id: set-package-list + name: Export list of generated packages + run: | + cd dist + set +x + ITEMS=$(ls navidrome_v* | sed 's/^navidrome_v[^_]*_linux_//' | jq -R -s -c 'split("\n")[:-1]') + echo $ITEMS + echo "package_list=${ITEMS}" >> $GITHUB_OUTPUT + + upload-packages: + name: Upload Linux PKG + runs-on: ubuntu-latest + needs: [release] + strategy: + matrix: + item: ${{ fromJson(needs.release.outputs.package_list) }} + steps: + - name: Download all-packages artifact + uses: actions/download-artifact@v4 + with: + name: packages + path: ./dist + + - name: Upload all-packages artifact + uses: actions/upload-artifact@v4 + with: + name: navidrome_linux_${{ matrix.item }} + path: dist/navidrome_v*_linux_${{ matrix.item }} + +# delete-artifacts: +# name: Delete unused artifacts +# runs-on: ubuntu-latest +# needs: [upload-packages] +# steps: +# - name: Delete all-packages artifact +# env: +# GH_TOKEN: ${{ github.token }} +# run: | +# for artifact in $(gh api repos/${{ github.repository }}/actions/artifacts | jq -r '.artifacts[] | select(.name | startswith("packages")) | .id'); do +# gh api --method DELETE repos/${{ github.repository }}/actions/artifacts/$artifact +# done \ No newline at end of file diff --git a/.gitignore b/.gitignore index 37999c3f9..e064113bf 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,7 @@ wiki TODO.md var navidrome.toml +!release/linux/navidrome.toml master.zip testDB cache/* diff --git a/Makefile b/Makefile index 234309cdb..e581bacd6 100644 --- a/Makefile +++ b/Makefile @@ -144,6 +144,11 @@ docker-msi: ##@Cross_Compilation Build MSI installer for Windows @du -h binaries/msi/*.msi .PHONY: docker-msi +package: docker-build ##@Cross_Compilation Create binaries and packages for ALL supported platforms + @if [ -z `which goreleaser` ]; then echo "Please install goreleaser first: https://goreleaser.com/install/"; exit 1; fi + goreleaser release -f release/goreleaser.yml --clean --skip=publish --snapshot +.PHONY: package + get-music: ##@Development Download some free music from Navidrome's demo instance mkdir -p music ( cd music; \ diff --git a/cmd/svc.go b/cmd/svc.go index 219339515..e277bd459 100644 --- a/cmd/svc.go +++ b/cmd/svc.go @@ -20,6 +20,9 @@ var ( service.StatusStopped: "Stopped", service.StatusRunning: "Running", } + + installUser string + workingDirectory string ) func init() { @@ -70,9 +73,11 @@ func (p *svcControl) Stop(service.Service) error { var svcInstance = sync.OnceValue(func() service.Service { options := make(service.KeyValue) - options["Restart"] = "on-success" + options["Restart"] = "on-failure" options["SuccessExitStatus"] = "1 2 8 SIGKILL" options["UserService"] = false + options["LogDirectory"] = conf.Server.DataFolder + options["SystemdScript"] = systemdScript if conf.Server.LogFile != "" { options["LogOutput"] = false } else { @@ -80,12 +85,13 @@ var svcInstance = sync.OnceValue(func() service.Service { options["LogDirectory"] = conf.Server.DataFolder } svcConfig := &service.Config{ + UserName: installUser, Name: "navidrome", DisplayName: "Navidrome", Description: "Your Personal Streaming Service", Dependencies: []string{ - "Requires=", - "After="}, + "After=remote-fs.target network.target", + }, WorkingDirectory: executablePath(), Option: options, } @@ -108,6 +114,10 @@ func runServiceCmd(cmd *cobra.Command, _ []string) { } func executablePath() string { + if workingDirectory != "" { + return workingDirectory + } + ex, err := os.Executable() if err != nil { log.Fatal(err) @@ -141,11 +151,15 @@ func buildInstallCmd() *cobra.Command { println("Service installed. Use 'navidrome svc start' to start it.") } - return &cobra.Command{ + cmd := &cobra.Command{ Use: "install", Short: "Install Navidrome service.", Run: runInstallCmd, } + cmd.Flags().StringVarP(&installUser, "user", "u", "", "user to run service") + cmd.Flags().StringVarP(&workingDirectory, "working-directory", "w", "", "working directory of service") + + return cmd } func buildUninstallCmd() *cobra.Command { @@ -216,3 +230,38 @@ func buildExecuteCmd() *cobra.Command { }, } } + +const systemdScript = `[Unit] +Description={{.Description}} +ConditionFileIsExecutable={{.Path|cmdEscape}} +{{range $i, $dep := .Dependencies}} +{{$dep}} {{end}} + +[Service] +StartLimitInterval=5 +StartLimitBurst=10 +ExecStart={{.Path|cmdEscape}}{{range .Arguments}} {{.|cmd}}{{end}} +{{if .WorkingDirectory}}WorkingDirectory={{.WorkingDirectory|cmdEscape}}{{end}} +{{if .UserName}}User={{.UserName}}{{end}} +{{if .Restart}}Restart={{.Restart}}{{end}} +{{if .SuccessExitStatus}}SuccessExitStatus={{.SuccessExitStatus}}{{end}} +TimeoutStopSec=20 +RestartSec=120 +EnvironmentFile=-/etc/sysconfig/{{.Name}} + +DevicePolicy=closed +NoNewPrivileges=yes +PrivateTmp=yes +ProtectControlGroups=yes +ProtectKernelModules=yes +ProtectKernelTunables=yes +RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6 +RestrictNamespaces=yes +RestrictRealtime=yes +SystemCallFilter=~@clock @debug @module @mount @obsolete @reboot @setuid @swap +{{if .WorkingDirectory}}ReadWritePaths={{.WorkingDirectory|cmdEscape}}{{end}} +ProtectSystem=full + +[Install] +WantedBy=multi-user.target +` diff --git a/contrib/navidrome.service b/contrib/navidrome.service index 817ab044c..5e6cbedce 100644 --- a/contrib/navidrome.service +++ b/contrib/navidrome.service @@ -11,15 +11,13 @@ WantedBy=multi-user.target User=navidrome Group=navidrome Type=simple -ExecStart=/usr/bin/navidrome +ExecStart=/usr/bin/navidrome --configfile "/etc/navidrome/navidrome.toml" StateDirectory=navidrome WorkingDirectory=/var/lib/navidrome TimeoutStopSec=20 KillMode=process Restart=on-failure -EnvironmentFile=-/etc/sysconfig/navidrome - # See https://www.freedesktop.org/software/systemd/man/systemd.exec.html CapabilityBoundingSet= DevicePolicy=closed diff --git a/release/goreleaser.yml b/release/goreleaser.yml index e353aecfd..59f724138 100644 --- a/release/goreleaser.yml +++ b/release/goreleaser.yml @@ -33,6 +33,58 @@ checksum: snapshot: version_template: "{{ .Tag }}-SNAPSHOT" +nfpms: + - id: navidrome + package_name: navidrome + + homepage: https://navidrome.org + description: |- + 🎧☁ Your Personal Streaming Service + + maintainer: Deluan Quintão + + license: GPL-3.0 + formats: + - deb + - rpm + + dependencies: + - ffmpeg + + suggests: + - mpv + + overrides: + rpm: + dependencies: + - "(ffmpeg or ffmpeg-free)" + + contents: + - src: release/linux/navidrome.toml + dst: /etc/navidrome/navidrome.toml + type: "config|noreplace" + file_info: + mode: 0644 + owner: navidrome + group: navidrome + + - dst: /var/lib/navidrome + type: dir + file_info: + owner: navidrome + group: navidrome + + - dst: /opt/navidrome/music + type: dir + file_info: + owner: navidrome + group: navidrome + + scripts: + preinstall: "release/linux/preinstall.sh" + postinstall: "release/linux/postinstall.sh" + preremove: "release/linux/preremove.sh" + release: draft: true mode: append diff --git a/release/linux/navidrome.toml b/release/linux/navidrome.toml new file mode 100644 index 000000000..e626c8caa --- /dev/null +++ b/release/linux/navidrome.toml @@ -0,0 +1,2 @@ +DataFolder = "/var/lib/navidrome" +MusicFolder = "/opt/navidrome/music" diff --git a/release/linux/postinstall.sh b/release/linux/postinstall.sh new file mode 100644 index 000000000..da43686b0 --- /dev/null +++ b/release/linux/postinstall.sh @@ -0,0 +1,25 @@ +#!/bin/sh + +# It is possible for a user to delete the configuration file in such a way that +# the package manager (in particular, deb) thinks that the file exists, while it is +# no longer on disk. Specifically, doing a `rm /etc/navidrome/navidrome.toml` +# without something like `apt purge navidrome` will result in the system believing that +# the file still exists. In this case, during isntall it will NOT extract the configuration +# file (as to not override it). Since `navidrome service install` depends on this file existing, +# we will create it with the defaults anyway. +if [ ! -f /etc/navidrome/navidrome.toml ]; then + printf "No navidrome.toml detected, creating in postinstall\n" + printf "DataFolder = \"/var/lib/navidrome\"\n" > /etc/navidrome/navidrome.toml + printf "MusicFolder = \"/opt/navidrome/music\"\n" >> /etc/navidrome/navidrome.toml +fi + +postinstall_flag="/var/lib/navidrome/.installed" + +if [ ! -f "$postinstall_flag" ]; then + # The primary reason why this would fail is if the service was already installed AND + # someone manually removed the .installed flag. In this case, ignore the error + navidrome service install --user navidrome --working-directory /var/lib/navidrome --configfile /etc/navidrome/navidrome.toml || : + touch "$postinstall_flag" +fi + + diff --git a/release/linux/preinstall.sh b/release/linux/preinstall.sh new file mode 100755 index 000000000..aa5850e6e --- /dev/null +++ b/release/linux/preinstall.sh @@ -0,0 +1,6 @@ +#!/bin/sh + +if ! getent passwd navidrome > /dev/null 2>&1; then + printf "Creating default Navidrome user\n" + useradd --home-dir /var/lib/navidrome --create-home --system --user-group navidrome +fi diff --git a/release/linux/preremove.sh b/release/linux/preremove.sh new file mode 100644 index 000000000..0dfcafe60 --- /dev/null +++ b/release/linux/preremove.sh @@ -0,0 +1,30 @@ +#!/bin/sh + +action=$1 + +remove() { + postinstall_flag="/var/lib/navidrome/.installed" + + if [ -f "$postinstall_flag" ]; then + # If this fails, ignore it + navidrome service uninstall || : + rm "$postinstall_flag" + + printf "The following may still be present (especially if you have not done a purge):\n" + printf "1. /etc/navidrome/navidrome.toml (configuration file)\n" + printf "2. /var/lib/navidrome (database/cache)\n" + printf "3. /opt/navidrome (default location for music)\n" + printf "4. The Navidrome user (user name navidrome)\n" + fi +} + +case "$action" in + "1" | "upgrade") + # For an upgrade, do nothing + # Leave the service file untouched + # This is relevant for RPM/DEB-based installs + ;; + *) + remove + ;; +esac