Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

sonarr: build from source #291640

Merged
merged 2 commits into from
Jul 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions nixos/modules/services/misc/sonarr.nix
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
{ config, pkgs, lib, ... }:
{ config, pkgs, lib, utils, ... }:

with lib;

Expand Down Expand Up @@ -54,7 +54,11 @@ in
Type = "simple";
User = cfg.user;
Group = cfg.group;
ExecStart = "${cfg.package}/bin/NzbDrone -nobrowser -data='${cfg.dataDir}'";
ExecStart = utils.escapeSystemdExecArgs [
(lib.getExe cfg.package)
"-nobrowser"
"-data=${cfg.dataDir}"
];
Restart = "on-failure";
};
};
Expand Down
357 changes: 357 additions & 0 deletions pkgs/by-name/so/sonarr/deps.nix

Large diffs are not rendered by default.

9 changes: 9 additions & 0 deletions pkgs/by-name/so/sonarr/nuget-config.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
Move NuGet configuration file to the source root where Nixpkgs .NET
build infrastructure expects to find it.

https://github.com/NixOS/nixpkgs/pull/291640#discussion_r1601841807

diff --git a/src/NuGet.Config b/NuGet.Config
similarity index 100%
rename from src/NuGet.Config
rename to NuGet.Config
161 changes: 161 additions & 0 deletions pkgs/by-name/so/sonarr/package.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
{ lib
, fetchFromGitHub
, buildDotnetModule
, dotnetCorePackages
, sqlite
, withFFmpeg ? true # replace bundled ffprobe binary with symlink to ffmpeg package.
, ffmpeg
, fetchYarnDeps
, yarn
, fixup-yarn-lock
, nodejs
, nixosTests
# update script
, writers
, python3Packages
, nix
, prefetch-yarn-deps
}:
let
version = "4.0.5.1710";
src = fetchFromGitHub {
owner = "Sonarr";
repo = "Sonarr";
rev = "v${version}";
hash = "sha256-9mrt5/6v8odPv1rwJoT6laXGlh3blgZAL97tsllj7MY=";
};
in
buildDotnetModule {
pname = "sonarr";
inherit version src;

patches = [
./nuget-config.patch
];

strictDeps = true;
nativeBuildInputs = [ nodejs yarn prefetch-yarn-deps fixup-yarn-lock ];

yarnOfflineCache = fetchYarnDeps {
yarnLock = "${src}/yarn.lock";
hash = "sha256-dSZBifvUGJx5lj7C+Sj+kJprK8JG6SE5vg6+X6QdCZ8=";
};

ffprobe = lib.optionalDrvAttr withFFmpeg (lib.getExe' ffmpeg "ffprobe");

postConfigure = ''
yarn config --offline set yarn-offline-mirror "$yarnOfflineCache"
fixup-yarn-lock yarn.lock
yarn install --offline --frozen-lockfile --ignore-platform --ignore-scripts --no-progress --non-interactive
patchShebangs --build node_modules
'';
postBuild = ''
yarn --offline run build --env production
'';
postInstall = lib.optionalString withFFmpeg ''
rm -- "$out/lib/sonarr/ffprobe"
ln -s -- "$ffprobe" "$out/lib/sonarr/ffprobe"
'' + ''
cp -a -- _output/UI "$out/lib/sonarr/UI"
'';
# Add an alias for compatibility with Sonarr v3 package.
postFixup = ''
ln -s -- Sonarr "$out/bin/NzbDrone"
'';

nugetDeps = ./deps.nix;

runtimeDeps = [ sqlite ];

dotnet-sdk = dotnetCorePackages.sdk_6_0;
dotnet-runtime = dotnetCorePackages.aspnetcore_6_0;

doCheck = true;

__darwinAllowLocalNetworking = true; # for tests

__structuredAttrs = true; # for Copyright property that contains spaces

executables = [ "Sonarr" ];
tie marked this conversation as resolved.
Show resolved Hide resolved

projectFile = [
"src/NzbDrone.Console/Sonarr.Console.csproj"
"src/NzbDrone.Mono/Sonarr.Mono.csproj"
];

testProjectFile = [
"src/NzbDrone.Api.Test/Sonarr.Api.Test.csproj"
"src/NzbDrone.Common.Test/Sonarr.Common.Test.csproj"
"src/NzbDrone.Core.Test/Sonarr.Core.Test.csproj"
"src/NzbDrone.Host.Test/Sonarr.Host.Test.csproj"
"src/NzbDrone.Libraries.Test/Sonarr.Libraries.Test.csproj"
"src/NzbDrone.Mono.Test/Sonarr.Mono.Test.csproj"
"src/NzbDrone.Test.Common/Sonarr.Test.Common.csproj"
];

dotnetFlags = [
"--property:TargetFramework=net6.0"
"--property:EnableAnalyzers=false"
# Override defaults in src/Directory.Build.props that use current time.
"--property:Copyright=Copyright 2014-2024 sonarr.tv (GNU General Public v3)"
"--property:AssemblyVersion=${version}"
"--property:AssemblyConfiguration=main"
];

# Skip manual, integration, automation and platform-dependent tests.
dotnetTestFlags = [
"--filter:${lib.concatStringsSep "&" [
"TestCategory!=ManualTest"
"TestCategory!=IntegrationTest"
"TestCategory!=AutomationTest"

# setgid tests
"FullyQualifiedName!=NzbDrone.Mono.Test.DiskProviderTests.DiskProviderFixture.should_preserve_setgid_on_set_folder_permissions"
"FullyQualifiedName!=NzbDrone.Mono.Test.DiskProviderTests.DiskProviderFixture.should_clear_setgid_on_set_folder_permissions"

# we do not set application data directory during tests (i.e. XDG data directory)
"FullyQualifiedName!=NzbDrone.Mono.Test.DiskProviderTests.FreeSpaceFixture.should_return_free_disk_space"

# attempts to read /etc/*release and fails since it does not exist
"FullyQualifiedName!=NzbDrone.Mono.Test.EnvironmentInfo.ReleaseFileVersionAdapterFixture.should_get_version_info"

# fails to start test dummy because it cannot locate .NET runtime for some reason
"FullyQualifiedName!=NzbDrone.Common.Test.ProcessProviderFixture.Should_be_able_to_start_process"
"FullyQualifiedName!=NzbDrone.Common.Test.ProcessProviderFixture.kill_all_should_kill_all_process_with_name"

# makes real HTTP requests
"FullyQualifiedName!~NzbDrone.Core.Test.TvTests.RefreshEpisodeServiceFixture"
"FullyQualifiedName!~NzbDrone.Core.Test.UpdateTests.UpdatePackageProviderFixture"

# fails on macOS
"FullyQualifiedName!~NzbDrone.Core.Test.Http.HttpProxySettingsProviderFixture"
]}"
];

passthru = {
tests = {
inherit (nixosTests) sonarr;
};

updateScript = writers.writePython3 "sonarr-updater"
{
libraries = with python3Packages; [ requests ];
makeWrapperArgs = [
"--prefix"
"PATH"
":"
(lib.makeBinPath [ nix prefetch-yarn-deps ])
];
}
./update.py;
};

meta = {
description = "Smart PVR for newsgroup and bittorrent users";
homepage = "https://sonarr.tv";
license = lib.licenses.gpl3Only;
maintainers = with lib.maintainers; [ fadenb purcell tie ];
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank goodness you're adding yourself as a maintainer for this, because maintaining this new version of the derivation is likely beyond my wizard level.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I’m, unfortunately, stuck with Sonarr 😅
It’s sad that it’s not fully self-hostable (it requires skyhook.sonarr.tv to be up and reachable for operation), but there are no other alternatives with comparable feature set, so it’s an important piece of infrastructure for me.

I don’t think there would be any non-trivial maintenance with source build aside from occasionally bumping .NET version, but feel free to ping me if there are any issues (besides, ofborg will ping me anyway).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm happy to help with general dotnet source-build stuff too.

mainProgram = "Sonarr";
# platforms inherited from dotnet-sdk.
};
}
160 changes: 160 additions & 0 deletions pkgs/by-name/so/sonarr/update.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
import json
import os
import pathlib
import requests
import shutil
import subprocess
import sys
import tempfile


def replace_in_file(file_path, replacements):
file_contents = pathlib.Path(file_path).read_text()
for old, new in replacements.items():
if old == new:
continue
updated_file_contents = file_contents.replace(old, new)
# A dumb way to check that we’ve actually replaced the string.
if file_contents == updated_file_contents:
print(f"no string to replace: {old} → {new}", file=sys.stderr)
sys.exit(1)
file_contents = updated_file_contents
with tempfile.NamedTemporaryFile(mode="w") as t:
t.write(file_contents)
t.flush()
shutil.copyfile(t.name, file_path)


def nix_hash_to_sri(hash):
return subprocess.run(
[
"nix",
"--extra-experimental-features", "nix-command",
"hash",
"to-sri",
"--type", "sha256",
"--",
hash,
],
stdout=subprocess.PIPE,
text=True,
check=True,
).stdout.rstrip()


nixpkgs_path = "."
attr_path = os.getenv("UPDATE_NIX_ATTR_PATH", "sonarr")

package_attrs = json.loads(subprocess.run(
[
"nix",
"--extra-experimental-features", "nix-command",
"eval",
"--json",
"--file", nixpkgs_path,
"--apply", """p: {
dir = builtins.dirOf p.meta.position;
version = p.version;
sourceHash = p.src.outputHash;
yarnHash = p.yarnOfflineCache.outputHash;
}""",
"--",
attr_path,
],
stdout=subprocess.PIPE,
text=True,
check=True,
).stdout)

old_version = package_attrs["version"]
new_version = old_version

# Note that we use Sonarr API instead of GitHub to fetch latest stable release.
# This corresponds to the Updates tab in the web UI. See also
# https://github.com/Sonarr/Sonarr/blob/070919a7e6a96ca7e26524996417c6f8d1b5fcaa/src/NzbDrone.Core/Update/UpdatePackageProvider.cs
version_update = requests.get(
f"https://services.sonarr.tv/v1/update/main?version={old_version}",
).json()
if version_update["available"]:
new_version = version_update["updatePackage"]["version"]

if new_version == old_version:
sys.exit()

source_nix_hash, source_store_path = subprocess.run(
[
"nix-prefetch-url",
"--name", "source",
"--unpack",
"--print-path",
f"https://github.com/Sonarr/Sonarr/archive/v{new_version}.tar.gz",
],
stdout=subprocess.PIPE,
text=True,
check=True,
).stdout.rstrip().split("\n")

old_source_hash = package_attrs["sourceHash"]
new_source_hash = nix_hash_to_sri(source_nix_hash)

old_yarn_hash = package_attrs["yarnHash"]
new_yarn_hash = nix_hash_to_sri(subprocess.run(
[
"prefetch-yarn-deps",
# does not support "--" separator :(
# Also --verbose writes to stdout, yikes.
os.path.join(source_store_path, "yarn.lock"),
],
stdout=subprocess.PIPE,
text=True,
check=True,
).stdout.rstrip())

package_dir = package_attrs["dir"]
package_file_name = "package.nix"
deps_file_name = "deps.nix"

# To update deps.nix, we copy the package to a temporary directory and run
# passthru.fetch-deps script there.
with tempfile.TemporaryDirectory() as work_dir:
package_file = os.path.join(work_dir, package_file_name)
deps_file = os.path.join(work_dir, deps_file_name)

shutil.copytree(package_dir, work_dir, dirs_exist_ok=True)

replace_in_file(package_file, {
# NB unlike hashes, versions are likely to be used in code or comments.
# Try to be more specific to avoid false positive matches.
f"version = \"{old_version}\"": f"version = \"{new_version}\"",
old_source_hash: new_source_hash,
old_yarn_hash: new_yarn_hash,
})

# Generate nuget-to-nix dependency lock file.
fetch_deps = os.path.join(work_dir, "fetch-deps")
subprocess.run(
[
"nix",
"--extra-experimental-features", "nix-command",
"build",
"--impure",
"--nix-path", "",
"--include", f"nixpkgs={nixpkgs_path}",
"--include", f"package={package_file}",
"--expr", "(import <nixpkgs> { }).callPackage <package> { }",
"--out-link", fetch_deps,
"passthru.fetch-deps",
],
check=True,
)
subprocess.run(
[
fetch_deps,
deps_file,
],
stdout=subprocess.DEVNULL,
check=True,
)

shutil.copy(deps_file, os.path.join(package_dir, deps_file_name))
shutil.copy(package_file, os.path.join(package_dir, package_file_name))
Loading