Skip to content

Build nix packages from source #253

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

Open
wants to merge 11 commits into
base: hovudstraum
Choose a base branch
from
Open
60 changes: 35 additions & 25 deletions contrib/package/nix/copyparty/default.nix
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
{
lib,
stdenv,
makeWrapper,
buildPythonApplication,
fetchurl,
util-linux,
python,
setuptools,
jinja2,
impacket,
pyopenssl,
Expand All @@ -15,6 +15,10 @@
pyzmq,
ffmpeg,
mutagen,
pyftpdlib,
magic,
partftpy,
fusepy, # for partyfuse

# use argon2id-hashed passwords in config files (sha2 is always available)
withHashedPasswords ? true,
Expand All @@ -40,12 +44,21 @@
# send ZeroMQ messages from event-hooks
withZeroMQ ? true,

# enable FTP server
withFTP ? true,

# enable FTPS support in the FTP server
withFTPS ? false,

# enable TFTP server
withTFTP ? false,

# samba/cifs server; dangerous and buggy, enable if you really need it
withSMB ? false,

# enables filetype detection for nameless uploads
withMagic ? false,

# extra packages to add to the PATH
extraPackages ? [ ],

Expand All @@ -58,40 +71,38 @@

let
pinData = lib.importJSON ./pin.json;
pyEnv = python.withPackages (
ps:
with ps;
runtimeDeps = ([ util-linux ] ++ extraPackages ++ lib.optional withMediaProcessing ffmpeg);
in
buildPythonApplication {
pname = "copyparty";
inherit (pinData) version;
src = fetchurl {
inherit (pinData) url hash;
};
Comment on lines +79 to +81
Copy link
Contributor

Choose a reason for hiding this comment

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

Just saying this is something that can be done, it's quite common for python packages in nixpkgs and I figured why not...

Suggested change
src = fetchurl {
inherit (pinData) url hash;
};
src = fetchPypi {
inherit (pinData) pname version hash;
};

I think this also allows us to move away from the custom update script, because nix-update has special support for PyPi packages. That's for another PR I think though.

Then all the info would be contained in the derivation, like this:

# no pin.json anymore
buildPythonApplication rec {
  pname = "copyparty";
  version = "1.18.8";
  src = fetchPyPi {
    inherit pname version;
    hash = "sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=";

And the update script would then be a measly nix-update copyparty --flake

Copy link
Contributor Author

Choose a reason for hiding this comment

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

While I do think it would be a good idea, if we were to do this @9001 would need to have nix installed to be able to update the nix packages (assuming that nix-update depends on nix), so we would need to know if he's ok with that.
If he is I'll be glad to do this in a separate PR tho

Copy link
Contributor

@dtomvan dtomvan Aug 2, 2025

Choose a reason for hiding this comment

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

Alternatively (maybe digging the hole even further in the process lol) you could have a Github action with on: workflow_dispatch that does the update with nix-update I guess...

assuming that nix-update depends on nix
Yeah, it needs nix to evaluate stuff. That's about the only advantage of the home-grown py+json solution right now: it doesn't involve any nix files... but at the same time that's a decoupling that usually isn't really desirable

TBF the updater for openjdk in nixpkgs also does this...

dependencies =
[
jinja2
fusepy
]
++ lib.optional withSMB impacket
++ lib.optional withFTP pyftpdlib
++ lib.optional withFTPS pyopenssl
++ lib.optional withTFTP partftpy
++ lib.optional withCertgen cfssl
++ lib.optional withThumbnails pillow
++ lib.optional withFastThumbnails pyvips
++ lib.optional withMediaProcessing ffmpeg
++ lib.optional withBasicAudioMetadata mutagen
++ lib.optional withHashedPasswords argon2-cffi
++ lib.optional withZeroMQ pyzmq
++ (extraPythonPackages ps)
);
++ lib.optional withMagic magic
++ (extraPythonPackages python.pkgs);
makeWrapperArgs = [ "--prefix PATH : ${lib.makeBinPath runtimeDeps}" ];

runtimeDeps = ([ util-linux ] ++ extraPackages ++ lib.optional withMediaProcessing ffmpeg);
in
stdenv.mkDerivation {
pname = "copyparty";
inherit (pinData) version;
src = fetchurl {
inherit (pinData) url hash;
};
nativeBuildInputs = [ makeWrapper ];
dontUnpack = true;
installPhase = ''
install -Dm755 $src $out/share/copyparty-sfx.py
makeWrapper ${pyEnv.interpreter} $out/bin/copyparty \
--prefix PATH : ${lib.makeBinPath runtimeDeps} \
--add-flag $out/share/copyparty-sfx.py
'';
pyproject = true;
build-system = [
setuptools
];
meta = {
description = "Turn almost any device into a file server";
longDescription = ''
Expand All @@ -101,8 +112,7 @@ stdenv.mkDerivation {
homepage = "https://github.com/9001/copyparty";
changelog = "https://github.com/9001/copyparty/releases/tag/v${pinData.version}";
license = lib.licenses.mit;
inherit (python.meta) platforms;
mainProgram = "copyparty";
sourceProvenance = [ lib.sourceTypes.binaryBytecode ];
sourceProvenance = [ lib.sourceTypes.fromSource ];
Copy link
Contributor

Choose a reason for hiding this comment

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

By the way this is implicitly the default when no sourceProvenance was specified.

};
}
4 changes: 2 additions & 2 deletions contrib/package/nix/copyparty/pin.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"url": "https://github.com/9001/copyparty/releases/download/v1.18.6/copyparty-sfx.py",
"url": "https://github.com/9001/copyparty/releases/download/v1.18.6/copyparty-1.18.6.tar.gz",
"version": "1.18.6",
"hash": "sha256-No89mzKHHZZH19ws9dqfvQO0pnZw7jKDMGhNa4LOFlY="
"hash": "sha256-gHYtkayIgV5z0MooBsY5Hc+MzVIbxALMMSNJ87yOiyg="
}
35 changes: 4 additions & 31 deletions contrib/package/nix/copyparty/update.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,18 @@

# Update the Nix package pin
#
# Usage: ./update.sh [PATH]
# When the [PATH] is not set, it will fetch the latest release from the repo.
# With [PATH] set, it will hash the given file and generate the URL,
# base on the version contained within the file
# Usage: ./update.sh

import base64
import json
import hashlib
import sys
import re
from pathlib import Path

OUTPUT_FILE = Path("pin.json")
TARGET_ASSET = "copyparty-sfx.py"
TARGET_ASSET = lambda version: f"copyparty-{version}.tar.gz"
HASH_TYPE = "sha256"
LATEST_RELEASE_URL = "https://api.github.com/repos/9001/copyparty/releases/latest"
DOWNLOAD_URL = lambda version: f"https://github.com/9001/copyparty/releases/download/v{version}/{TARGET_ASSET}"


def get_formatted_hash(binary):
Expand All @@ -29,20 +24,12 @@ def get_formatted_hash(binary):
return f"{HASH_TYPE}-{encoded_hash}"


def version_from_sfx(binary):
result = re.search(b'^VER = "(.*)"$', binary, re.MULTILINE)
if result:
return result.groups(1)[0].decode("ascii")

raise ValueError("version not found in provided file")


def remote_release_pin():
import requests

response = requests.get(LATEST_RELEASE_URL).json()
version = response["tag_name"].lstrip("v")
asset_info = [a for a in response["assets"] if a["name"] == TARGET_ASSET][0]
asset_info = [a for a in response["assets"] if a["name"] == TARGET_ASSET(version)][0]
download_url = asset_info["browser_download_url"]
asset = requests.get(download_url)
formatted_hash = get_formatted_hash(asset.content)
Expand All @@ -51,22 +38,8 @@ def remote_release_pin():
return result


def local_release_pin(path):
asset = path.read_bytes()
version = version_from_sfx(asset)
download_url = DOWNLOAD_URL(version)
formatted_hash = get_formatted_hash(asset)

result = {"url": download_url, "version": version, "hash": formatted_hash}
return result


def main():
if len(sys.argv) > 1:
asset_path = Path(sys.argv[1])
result = local_release_pin(asset_path)
else:
result = remote_release_pin()
result = remote_release_pin()

print(result)
json_result = json.dumps(result, indent=4)
Expand Down
30 changes: 30 additions & 0 deletions contrib/package/nix/partftpy/default.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
{
lib,
buildPythonPackage,
fetchurl,
setuptools,
}:
let
pinData = lib.importJSON ./pin.json;
in

buildPythonPackage rec {
pname = "partftpy";
inherit (pinData) version;
pyproject = true;

src = fetchurl {
inherit (pinData) url hash;
};

build-system = [ setuptools ];

pythonImportsCheck = [ "partftpy.TftpServer" ];

meta = {
description = "Pure Python TFTP library (copyparty edition)";
homepage = "https://github.com/9001/partftpy";
changelog = "https://github.com/9001/partftpy/releases/tag/${version}";
license = lib.licenses.mit;
};
}
5 changes: 5 additions & 0 deletions contrib/package/nix/partftpy/pin.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"url": "https://github.com/9001/partftpy/releases/download/v0.4.0/partftpy-0.4.0.tar.gz",
"version": "0.4.0",
"hash": "sha256-5Q2zyuJ892PGZmb+YXg0ZPW/DK8RDL1uE0j5HPd4We0="
}
50 changes: 50 additions & 0 deletions contrib/package/nix/partftpy/update.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
#!/usr/bin/env python3

# Update the Nix package pin
#
# Usage: ./update.sh

import base64
import json
import hashlib
import sys
from pathlib import Path

OUTPUT_FILE = Path("pin.json")
TARGET_ASSET = lambda version: f"partftpy-{version}.tar.gz"
HASH_TYPE = "sha256"
LATEST_RELEASE_URL = "https://api.github.com/repos/9001/partftpy/releases/latest"


def get_formatted_hash(binary):
hasher = hashlib.new("sha256")
hasher.update(binary)
asset_hash = hasher.digest()
encoded_hash = base64.b64encode(asset_hash).decode("ascii")
return f"{HASH_TYPE}-{encoded_hash}"


def remote_release_pin():
import requests

response = requests.get(LATEST_RELEASE_URL).json()
version = response["tag_name"].lstrip("v")
asset_info = [a for a in response["assets"] if a["name"] == TARGET_ASSET(version)][0]
download_url = asset_info["browser_download_url"]
asset = requests.get(download_url)
formatted_hash = get_formatted_hash(asset.content)

result = {"url": download_url, "version": version, "hash": formatted_hash}
return result


def main():
result = remote_release_pin()

print(result)
json_result = json.dumps(result, indent=4)
OUTPUT_FILE.write_text(json_result)


if __name__ == "__main__":
main()
26 changes: 0 additions & 26 deletions contrib/package/nix/partyfuse/default.nix

This file was deleted.

24 changes: 0 additions & 24 deletions contrib/package/nix/u2c/default.nix

This file was deleted.

15 changes: 5 additions & 10 deletions flake.nix
Original file line number Diff line number Diff line change
Expand Up @@ -12,17 +12,14 @@
}:
{
nixosModules.default = ./contrib/nixos/modules/copyparty.nix;
overlays.default = final: prev: rec {
overlays.default = final: prev: {
copyparty = final.python3.pkgs.callPackage ./contrib/package/nix/copyparty {
ffmpeg = final.ffmpeg-full;
};

partyfuse = prev.callPackage ./contrib/package/nix/partyfuse {
inherit copyparty;
};

u2c = prev.callPackage ./contrib/package/nix/u2c {
inherit copyparty;
python3 = prev.python3.override {
packageOverrides = pyFinal: pyPrev: {
partftpy = pyFinal.callPackage ./contrib/package/nix/partftpy { };
};
};
};
}
Expand Down Expand Up @@ -54,8 +51,6 @@
packages = {
inherit (pkgs)
copyparty
partyfuse
u2c
;
default = self.packages.${system}.copyparty;
};
Expand Down