Compare commits
1 commit
Author | SHA1 | Date | |
---|---|---|---|
a89ca47df7 |
18 changed files with 691 additions and 264 deletions
15
hive.nix
15
hive.nix
|
@ -191,11 +191,9 @@ in
|
|||
# Deployment config is specified in meta.nodes.${node}.deployment
|
||||
inherit (nodeMeta) deployment;
|
||||
|
||||
# Set NIX_PATH to the patched version of nixpkgs
|
||||
environment.etc.nixpkgs.source = builtins.storePath sourcePkgs.path;
|
||||
nix.nixPath = [ "nixpkgs=/etc/nixpkgs" ];
|
||||
|
||||
nix = {
|
||||
# Set NIX_PATH to the patched version of nixpkgs
|
||||
nixPath = [ "nixpkgs=${builtins.storePath sourcePkgs.path}" ];
|
||||
optimise.automatic = true;
|
||||
|
||||
gc = {
|
||||
|
@ -208,7 +206,14 @@ in
|
|||
};
|
||||
|
||||
# Allow unfree packages
|
||||
nixpkgs.config.allowUnfree = true;
|
||||
nixpkgs = {
|
||||
config.allowUnfree = true;
|
||||
overlays = [
|
||||
(self: _: {
|
||||
lix-diff = self.callPackage (sources.lix-diff + "/package.nix") { };
|
||||
})
|
||||
];
|
||||
};
|
||||
|
||||
# Use the stateVersion declared in the metadata
|
||||
system = {
|
||||
|
|
|
@ -71,31 +71,12 @@ rec {
|
|||
src,
|
||||
name,
|
||||
patches ? mkPatches name,
|
||||
prePatch ? null,
|
||||
postPatch ? null,
|
||||
...
|
||||
}@args:
|
||||
if patches == [ ] && prePatch == null && postPatch == null then
|
||||
src
|
||||
else
|
||||
pkgs.stdenvNoCC.mkDerivation (
|
||||
args
|
||||
// {
|
||||
name = "${name}-patched";
|
||||
}:
|
||||
pkgs.applyPatches {
|
||||
inherit patches src;
|
||||
|
||||
inherit patches prePatch postPatch;
|
||||
|
||||
preferLocalBuild = true;
|
||||
allowSubstitutes = true;
|
||||
phases = [
|
||||
"unpackPhase"
|
||||
"patchPhase"
|
||||
"installPhase"
|
||||
];
|
||||
|
||||
installPhase = "cp -R . $out";
|
||||
}
|
||||
);
|
||||
name = "${name}-patched";
|
||||
};
|
||||
|
||||
applyPatches' = name: src: applyPatches { inherit name src; };
|
||||
};
|
||||
|
|
46
lon.lock
generated
46
lon.lock
generated
|
@ -7,9 +7,9 @@
|
|||
"owner": "ryantm",
|
||||
"repo": "agenix",
|
||||
"branch": "main",
|
||||
"revision": "531beac616433bac6f9e2a19feb8e99a22a66baf",
|
||||
"url": "https://github.com/ryantm/agenix/archive/531beac616433bac6f9e2a19feb8e99a22a66baf.tar.gz",
|
||||
"hash": "sha256-9P1FziAwl5+3edkfFcr5HeGtQUtrSdk/MksX39GieoA="
|
||||
"revision": "4835b1dc898959d8547a871ef484930675cb47f1",
|
||||
"url": "https://github.com/ryantm/agenix/archive/4835b1dc898959d8547a871ef484930675cb47f1.tar.gz",
|
||||
"hash": "sha256-NwmAFuDUO/PFcgaGGr4j3ozG9Pe5hZ/ogitWhY+D81k="
|
||||
},
|
||||
"arkheon": {
|
||||
"type": "GitHub",
|
||||
|
@ -105,10 +105,10 @@
|
|||
"type": "Git",
|
||||
"fetchType": "git",
|
||||
"branch": "master",
|
||||
"revision": "3838db6ebbfe5ad9f904ce553543c1c301b67274",
|
||||
"revision": "19b3de953c4d4e8888b90019db81852f8ad39dbb",
|
||||
"url": "https://git.dgnum.eu/lbailly/kat-pkgs",
|
||||
"hash": "sha256-ifgYL9gJ1XKEL45WdFqGM17r5ZUkLnTuV2tGk+ie80I=",
|
||||
"lastModified": 1750258895,
|
||||
"hash": "sha256-bWO5dHrwZWF2EbCuSzxigaKkJdNCBQx5nD1J/u2pdNg=",
|
||||
"lastModified": 1749652165,
|
||||
"submodules": false
|
||||
},
|
||||
"liminix": {
|
||||
|
@ -135,12 +135,22 @@
|
|||
"type": "Git",
|
||||
"fetchType": "git",
|
||||
"branch": "main",
|
||||
"revision": "20fed838a622e48128827278db91312f580f9214",
|
||||
"revision": "ee0655240270480d7f6063dcf12ec47f04d2ded6",
|
||||
"url": "https://git.lix.systems/lix-project/lix.git",
|
||||
"hash": "sha256-Swcajzm+JPDd32kKXdg25im9CeATuY8qji9EPVU2rVo=",
|
||||
"lastModified": 1750232556,
|
||||
"hash": "sha256-DDhns3NS6L5OlYR0mSX03I5D7uGLyyd3MZegd1wTCyc=",
|
||||
"lastModified": 1749682763,
|
||||
"submodules": false
|
||||
},
|
||||
"lix-diff": {
|
||||
"type": "GitHub",
|
||||
"fetchType": "tarball",
|
||||
"owner": "tgirlcloud",
|
||||
"repo": "lix-diff",
|
||||
"branch": "main",
|
||||
"revision": "3d52194bc78f0d7478c6d1a900f2c806c643b995",
|
||||
"url": "https://github.com/tgirlcloud/lix-diff/archive/3d52194bc78f0d7478c6d1a900f2c806c643b995.tar.gz",
|
||||
"hash": "sha256-apjYXFdvxLZjhcN1wV7Y/LKNuWtWtCZM0h1VFg/znVo="
|
||||
},
|
||||
"lix-module": {
|
||||
"type": "Git",
|
||||
"fetchType": "git",
|
||||
|
@ -157,9 +167,9 @@
|
|||
"owner": "nikstur",
|
||||
"repo": "lon",
|
||||
"branch": "main",
|
||||
"revision": "c44e33ce55eed38a06fde43e69512380c4065441",
|
||||
"url": "https://github.com/nikstur/lon/archive/c44e33ce55eed38a06fde43e69512380c4065441.tar.gz",
|
||||
"hash": "sha256-bxu83mbdfAeDZYOnjZQYyjTs5WgZS8o6Q2irlzgbYs0="
|
||||
"revision": "c29151c0adefbf2eef904a3435350356cef98da2",
|
||||
"url": "https://github.com/nikstur/lon/archive/c29151c0adefbf2eef904a3435350356cef98da2.tar.gz",
|
||||
"hash": "sha256-1oQ4uLI92Ih2rmNyP4wzP9xZrQp48FHirOhV/aerZPc="
|
||||
},
|
||||
"metis": {
|
||||
"type": "Git",
|
||||
|
@ -195,10 +205,10 @@
|
|||
"type": "Git",
|
||||
"fetchType": "git",
|
||||
"branch": "dgnum",
|
||||
"revision": "fd4ba193ea3eda529ac27b43b206e9e3618b1975",
|
||||
"revision": "44ccf96bd73c1bbbbcc849cb0f2e0d1f5f75f934",
|
||||
"url": "https://git.hubrecht.ovh/hubrecht/nix-modules",
|
||||
"hash": "sha256-O/lMCM0qKkd+TBV43Fp9uG3aEbDSc2lI3a5TetNYs0w=",
|
||||
"lastModified": 1749739595,
|
||||
"hash": "sha256-mkrCWowrCje3/TuAG0eAJplrtlz1hYmusSFn93/Ccok=",
|
||||
"lastModified": 1749629064,
|
||||
"submodules": false
|
||||
},
|
||||
"nix-pkgs": {
|
||||
|
@ -285,10 +295,10 @@
|
|||
"type": "Git",
|
||||
"fetchType": "git",
|
||||
"branch": "main",
|
||||
"revision": "f3d0a3146c64f8fe6bdb208b75cc680c96f524e1",
|
||||
"revision": "62346b99c2e1085203bc2e5bb5f07e7773977b49",
|
||||
"url": "https://git.dgnum.eu/DGNum/snix-cache.git",
|
||||
"hash": "sha256-D6NRGdsIwvXf9MxTR1gFreefBKM3giFh8ggTM6wsh8o=",
|
||||
"lastModified": 1750061908,
|
||||
"hash": "sha256-6BYUWwzitWF2EV8wvJOlqensJ3x4f4ka+iZ9Zy5XnWI=",
|
||||
"lastModified": 1744711329,
|
||||
"submodules": false
|
||||
},
|
||||
"stateless-uptime-kuma": {
|
||||
|
|
|
@ -28,7 +28,6 @@ lib.extra.mkConfig {
|
|||
"mastodon"
|
||||
# "netbox"
|
||||
"nextcloud"
|
||||
"nimbolus"
|
||||
"ollama-proxy"
|
||||
"opengist"
|
||||
"outline"
|
||||
|
|
|
@ -1,43 +0,0 @@
|
|||
# SPDX-FileCopyrightText: 2025 Lubin Bailly <lubin@dgnum.eu>
|
||||
#
|
||||
# SPDX-License-Identifier: EUPL-1.2
|
||||
|
||||
{
|
||||
pkgs,
|
||||
sources,
|
||||
config,
|
||||
...
|
||||
}:
|
||||
let
|
||||
host = "nimbolus.dgnum.eu";
|
||||
port = 9008;
|
||||
in
|
||||
{
|
||||
imports = [ ./module.nix ];
|
||||
services.nimbolus-tf = {
|
||||
enable = true;
|
||||
package = (import sources.kat-pkgs { inherit pkgs; }).nimbolus-tf-backend;
|
||||
settings = {
|
||||
LISTEN_ADDR = "127.0.0.1:${toString port}";
|
||||
|
||||
STORAGE_BACKEND = "s3";
|
||||
STORAGE_S3_ENDPOINT = "s3.dgnum.eu";
|
||||
STORAGE_S3_USE_SSL = "true";
|
||||
STORAGE_S3_BUCKET = "nimbolus-dgnum";
|
||||
STORAGE_S3_ACCESS_KEY = "GKefa111701f349de3988f0010";
|
||||
|
||||
# TODO: configure openBAO
|
||||
# AUTH_BASIC_ENABLED = "false";
|
||||
# AUTH_JWT_OIDC_ISSUER_URL = "https://vault.dgnum.eu/v1/identity/oidc";
|
||||
};
|
||||
|
||||
credentials = {
|
||||
KMS_KEY_FILE = config.age.secrets."nimbolus-kms_key".path;
|
||||
STORAGE_S3_SECRET_KEY_FILE = config.age.secrets."nimbolus-s3_secret".path;
|
||||
};
|
||||
};
|
||||
|
||||
dgn-web.simpleProxies.nimbolus = {
|
||||
inherit host port;
|
||||
};
|
||||
}
|
|
@ -1,104 +0,0 @@
|
|||
# SPDX-FileCopyrightText: 2025 Lubin Bailly <lubin@dgnum.eu>
|
||||
#
|
||||
# SPDX-License-Identifier: EUPL-1.2
|
||||
|
||||
{
|
||||
lib,
|
||||
config,
|
||||
sources,
|
||||
pkgs,
|
||||
...
|
||||
}:
|
||||
let
|
||||
inherit (lib)
|
||||
getExe
|
||||
mapAttrsToList
|
||||
mkEnableOption
|
||||
mkIf
|
||||
mkPackageOption
|
||||
mkOption
|
||||
;
|
||||
inherit (lib.types)
|
||||
attrsOf
|
||||
path
|
||||
str
|
||||
;
|
||||
|
||||
cfg = config.services.nimbolus-tf;
|
||||
in
|
||||
{
|
||||
options.services.nimbolus-tf = {
|
||||
enable = mkEnableOption "the nimbolus terraform http backend";
|
||||
package = mkPackageOption (import sources.kat-pkgs { inherit pkgs; }) "nimbolus-tf-backend" {
|
||||
pkgsText = "kat-pkgs";
|
||||
};
|
||||
user = mkOption {
|
||||
type = str;
|
||||
description = ''
|
||||
User used by the nimbolus server.
|
||||
'';
|
||||
default = "nimbolus";
|
||||
};
|
||||
group = mkOption {
|
||||
type = str;
|
||||
description = ''
|
||||
Group used by the nimbolus server.
|
||||
'';
|
||||
default = "nimbolus";
|
||||
};
|
||||
settings = mkOption {
|
||||
type = attrsOf str;
|
||||
default = { };
|
||||
description = ''
|
||||
Environment variables for nimbolus configuration.
|
||||
'';
|
||||
};
|
||||
credentials = mkOption {
|
||||
type = attrsOf path;
|
||||
default = { };
|
||||
description = ''
|
||||
Files to pass by systemd LoadCredentials.
|
||||
'';
|
||||
};
|
||||
};
|
||||
config = mkIf cfg.enable {
|
||||
systemd.services.nimbolus-tf = {
|
||||
description = "Nimbolus terraform http backend";
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
serviceConfig = {
|
||||
ExecStart = getExe cfg.package;
|
||||
Environment =
|
||||
mapAttrsToList (name: value: "${name}=${value}") cfg.settings
|
||||
++ mapAttrsToList (name: _: "${name}=%d/${name}") cfg.credentials;
|
||||
LoadCredential = mapAttrsToList (name: file: "${name}:${file}") cfg.credentials;
|
||||
|
||||
StateDirectory = "nimbolus-tf";
|
||||
StateDirectoryMode = "0700";
|
||||
WorkingDirectory = "/var/lib/nimbolus-tf";
|
||||
|
||||
# Hardening
|
||||
DynamicUser = true;
|
||||
CapabilityBoundingSet = "";
|
||||
PrivateDevices = true;
|
||||
ProtectClock = true;
|
||||
ProtectKernelLogs = true;
|
||||
ProtectControlGroups = true;
|
||||
ProtectKernelModules = true;
|
||||
RestrictNamespaces = true;
|
||||
ProtectHostname = true;
|
||||
LockPersonality = true;
|
||||
RestrictRealtime = true;
|
||||
ProtectHome = true;
|
||||
ProtectProc = "noaccess";
|
||||
ProcSubset = "pid";
|
||||
PrivateUsers = true;
|
||||
UMask = "0077";
|
||||
ProtectKernelTunables = true;
|
||||
RestrictAddressFamilies = "AF_INET AF_INET6";
|
||||
SystemCallFilter = "~@clock @cpu-emulation @debug @module @mount @obsolete @privileged @raw-io @reboot @resources @swap";
|
||||
MemoryDenyWriteExecute = true;
|
||||
SystemCallArchitectures = "native";
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
Binary file not shown.
Binary file not shown.
|
@ -25,8 +25,6 @@
|
|||
"netbox-environment_file"
|
||||
"nextcloud-adminpass_file"
|
||||
"nextcloud-s3_secret_file"
|
||||
"nimbolus-kms_key"
|
||||
"nimbolus-s3_secret"
|
||||
"opengist-environment_file"
|
||||
"outline-oidc_client_secret_file"
|
||||
"outline-smtp_password_file"
|
||||
|
|
|
@ -19,10 +19,5 @@ in
|
|||
};
|
||||
};
|
||||
|
||||
fileSystems."/var/lib/victorialogs" = {
|
||||
device = "/data/fast/victorialogs";
|
||||
options = [ "bind" ];
|
||||
};
|
||||
|
||||
networking.firewall.interfaces.wt0.allowedTCPPorts = [ port ];
|
||||
}
|
||||
|
|
|
@ -82,7 +82,6 @@ let
|
|||
"gist" # Opengist
|
||||
"grafana" # Grafana
|
||||
"netbox-v2" # Netbox
|
||||
"nimbolus" # Nimbolus Terraform Backend
|
||||
"nms" # LibreNMS
|
||||
"pads" # Hedgedoc
|
||||
"pass" # Vaultwarden
|
||||
|
|
|
@ -37,9 +37,8 @@
|
|||
"dgn-web"
|
||||
"django-apps"
|
||||
"extranix"
|
||||
"forgejo-multiuser-nix-runners"
|
||||
"openbao"
|
||||
"systemd-notify"
|
||||
"forgejo-multiuser-nix-runners"
|
||||
])
|
||||
++ [
|
||||
"${sources.agenix}/modules/age.nix"
|
||||
|
@ -53,6 +52,7 @@
|
|||
"services/forgejo-nix-runners"
|
||||
"services/nginx-sni"
|
||||
"services/reaction"
|
||||
"services/systemd-notify"
|
||||
"services/victorialogs"
|
||||
"services/victoriametrics"
|
||||
]
|
||||
|
|
|
@ -84,7 +84,7 @@ in
|
|||
system.activationScripts.diff = {
|
||||
supportsDryActivation = true;
|
||||
text = ''
|
||||
${pkgs.nvd}/bin/nvd --nix-bin-dir=${pkgs.nix}/bin diff /run/current-system "$systemConfig"
|
||||
${lib.getExe pkgs.lix-diff} --lix-bin ${pkgs.nix}/bin /run/current-system $systemConfig
|
||||
'';
|
||||
};
|
||||
|
||||
|
@ -92,6 +92,7 @@ in
|
|||
|
||||
environment.systemPackages =
|
||||
(with pkgs; [
|
||||
neovim
|
||||
wget
|
||||
kitty.terminfo
|
||||
|
||||
|
|
|
@ -54,16 +54,19 @@ in
|
|||
};
|
||||
|
||||
services.systemd-notify = {
|
||||
mail = pkgs.writeShellScriptBin "sendmail" ''
|
||||
${pkgs.msmtp}/bin/sendmail -i -t <<ERRMAIL
|
||||
To: admins+monitoring@dgnum.eu, ${emails}
|
||||
Subject: [$HOSTNAME] Systemd failure: $1
|
||||
Content-Transfer-Encoding: 8bit
|
||||
Content-Type: text/plain; charset=UTF-8
|
||||
enable = true;
|
||||
command = builtins.toString (
|
||||
pkgs.writeShellScript "sendmail" ''
|
||||
${pkgs.msmtp}/bin/sendmail -i -t <<ERRMAIL
|
||||
To: admins+monitoring@dgnum.eu, ${emails}
|
||||
Subject: [$HOSTNAME] Systemd failure: $1
|
||||
Content-Transfer-Encoding: 8bit
|
||||
Content-Type: text/plain; charset=UTF-8
|
||||
|
||||
$(systemctl status --full "$1")
|
||||
ERRMAIL
|
||||
'';
|
||||
$(systemctl status --full "$1")
|
||||
ERRMAIL
|
||||
''
|
||||
);
|
||||
};
|
||||
age-secrets.sources = [ ./. ];
|
||||
};
|
||||
|
|
|
@ -1,49 +0,0 @@
|
|||
# SPDX-FileCopyrightText: 2024 Tom Hubrecht <tom.hubrecht@dgnum.eu>
|
||||
#
|
||||
# SPDX-License-Identifier: EUPL-1.2
|
||||
|
||||
{ config, lib, ... }:
|
||||
|
||||
let
|
||||
inherit (lib)
|
||||
getExe
|
||||
mapAttrs'
|
||||
mapAttrsToList
|
||||
mkOption
|
||||
mkForce
|
||||
nameValuePair
|
||||
;
|
||||
inherit (lib.types) attrsOf package submodule;
|
||||
|
||||
cfg = config.services.systemd-notify;
|
||||
in
|
||||
|
||||
{
|
||||
options.services.systemd-notify = mkOption {
|
||||
type = attrsOf package;
|
||||
description = ''
|
||||
Commands to execute when a systemd unit fails.
|
||||
Attrs keys will be the unit name and attrs value is the command that
|
||||
will be run with the name of the failed unit as an argument.
|
||||
'';
|
||||
default = { };
|
||||
};
|
||||
|
||||
options.systemd.services = mkOption {
|
||||
type = attrsOf (submodule {
|
||||
config.onFailure = mapAttrsToList (name: _: "${name}@%n.service") cfg;
|
||||
});
|
||||
};
|
||||
|
||||
config.systemd.services = mapAttrs' (
|
||||
name: script:
|
||||
nameValuePair "${name}@" {
|
||||
description = "Run ${name} script on service failures.";
|
||||
onFailure = mkForce [ ]; # Avoid recursive failures
|
||||
serviceConfig = {
|
||||
ExecStart = "${getExe script} %i";
|
||||
Type = "oneshot";
|
||||
};
|
||||
}
|
||||
) cfg;
|
||||
}
|
|
@ -20,6 +20,10 @@ with {
|
|||
(local ./lix/01-disable-installChecks.patch)
|
||||
];
|
||||
|
||||
lon = [
|
||||
(local ./lon/01-npins-import.patch)
|
||||
];
|
||||
|
||||
"nixos-25.05" = [
|
||||
# Crabfit: don't depend on all google-fonts
|
||||
(local ./nixpkgs/03-crabfit-karla.patch)
|
||||
|
|
625
patches/lon/01-npins-import.patch
Normal file
625
patches/lon/01-npins-import.patch
Normal file
|
@ -0,0 +1,625 @@
|
|||
From 70877569a4ce8f5274c5e6208469c240a34993a0 Mon Sep 17 00:00:00 2001
|
||||
From: Tom Hubrecht <tom@hubrecht.ovh>
|
||||
Date: Tue, 10 Jun 2025 15:26:22 +0200
|
||||
Subject: [PATCH 1/2] sources: Find default branch when none is supplied
|
||||
|
||||
---
|
||||
rust/lon/Cargo.lock | 33 +++++++++++++++++++++++++++++++++
|
||||
rust/lon/Cargo.toml | 1 +
|
||||
rust/lon/src/cli.rs | 8 ++++----
|
||||
rust/lon/src/git.rs | 29 +++++++++++++++++++++++++++++
|
||||
rust/lon/src/init/niv.rs | 4 ++--
|
||||
rust/lon/src/sources.rs | 18 +++++++++++++++---
|
||||
6 files changed, 84 insertions(+), 9 deletions(-)
|
||||
|
||||
diff --git a/rust/lon/Cargo.lock b/rust/lon/Cargo.lock
|
||||
index 62f6176..b9e7944 100644
|
||||
--- a/rust/lon/Cargo.lock
|
||||
+++ b/rust/lon/Cargo.lock
|
||||
@@ -17,6 +17,15 @@ version = "2.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627"
|
||||
|
||||
+[[package]]
|
||||
+name = "aho-corasick"
|
||||
+version = "1.1.3"
|
||||
+source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
+checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916"
|
||||
+dependencies = [
|
||||
+ "memchr",
|
||||
+]
|
||||
+
|
||||
[[package]]
|
||||
name = "android-tzdata"
|
||||
version = "0.1.1"
|
||||
@@ -847,6 +856,7 @@ dependencies = [
|
||||
"expect-test",
|
||||
"indoc",
|
||||
"log",
|
||||
+ "regex",
|
||||
"reqwest",
|
||||
"serde",
|
||||
"serde_json",
|
||||
@@ -1073,11 +1083,34 @@ dependencies = [
|
||||
"getrandom 0.3.2",
|
||||
]
|
||||
|
||||
+[[package]]
|
||||
+name = "regex"
|
||||
+version = "1.11.1"
|
||||
+source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
+checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191"
|
||||
+dependencies = [
|
||||
+ "aho-corasick",
|
||||
+ "memchr",
|
||||
+ "regex-automata",
|
||||
+ "regex-syntax",
|
||||
+]
|
||||
+
|
||||
[[package]]
|
||||
name = "regex-automata"
|
||||
version = "0.4.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908"
|
||||
+dependencies = [
|
||||
+ "aho-corasick",
|
||||
+ "memchr",
|
||||
+ "regex-syntax",
|
||||
+]
|
||||
+
|
||||
+[[package]]
|
||||
+name = "regex-syntax"
|
||||
+version = "0.8.5"
|
||||
+source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
+checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
|
||||
|
||||
[[package]]
|
||||
name = "reqwest"
|
||||
diff --git a/rust/lon/Cargo.toml b/rust/lon/Cargo.toml
|
||||
index a60c24e..d7dd633 100644
|
||||
--- a/rust/lon/Cargo.toml
|
||||
+++ b/rust/lon/Cargo.toml
|
||||
@@ -13,6 +13,7 @@ serde_json = "1.0.140"
|
||||
sha2 = "0.10.9"
|
||||
tempfile = "3.20.0"
|
||||
reqwest = { version = "0.12", default-features = false, features = ["blocking","http2","rustls-tls","json"] }
|
||||
+regex = "1.11.1"
|
||||
|
||||
[dev-dependencies]
|
||||
expect-test = "1.5.1"
|
||||
diff --git a/rust/lon/src/cli.rs b/rust/lon/src/cli.rs
|
||||
index eb850d7..5806b1d 100644
|
||||
--- a/rust/lon/src/cli.rs
|
||||
+++ b/rust/lon/src/cli.rs
|
||||
@@ -105,7 +105,7 @@ struct AddGitArgs {
|
||||
/// URL to the repository
|
||||
url: String,
|
||||
/// Branch to track
|
||||
- branch: String,
|
||||
+ branch: Option<String>,
|
||||
/// Revision to lock
|
||||
#[arg(short, long)]
|
||||
revision: Option<String>,
|
||||
@@ -122,7 +122,7 @@ struct AddGitHubArgs {
|
||||
/// An identifier made up of {owner}/{repo}, e.g. nixos/nixpkgs
|
||||
identifier: String,
|
||||
/// Branch to track
|
||||
- branch: String,
|
||||
+ branch: Option<String>,
|
||||
/// Name of the source
|
||||
///
|
||||
/// If you do not supply this, the repository name is used as the source name.
|
||||
@@ -283,7 +283,7 @@ fn add_git(directory: impl AsRef<Path>, args: &AddGitArgs) -> Result<()> {
|
||||
|
||||
let source = GitSource::new(
|
||||
&args.url,
|
||||
- &args.branch,
|
||||
+ args.branch.as_ref(),
|
||||
args.revision.as_ref(),
|
||||
args.submodules,
|
||||
args.frozen,
|
||||
@@ -314,7 +314,7 @@ fn add_github(directory: impl AsRef<Path>, args: &AddGitHubArgs) -> Result<()> {
|
||||
let source = GitHubSource::new(
|
||||
owner,
|
||||
repo,
|
||||
- &args.branch,
|
||||
+ args.branch.as_ref(),
|
||||
args.revision.as_ref(),
|
||||
args.frozen,
|
||||
)?;
|
||||
diff --git a/rust/lon/src/git.rs b/rust/lon/src/git.rs
|
||||
index cb5b4df..381c337 100644
|
||||
--- a/rust/lon/src/git.rs
|
||||
+++ b/rust/lon/src/git.rs
|
||||
@@ -5,6 +5,7 @@ use std::{
|
||||
};
|
||||
|
||||
use anyhow::{Context, Result, bail};
|
||||
+use regex::Regex;
|
||||
use tempfile::TempDir;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
@@ -129,6 +130,34 @@ fn find_newest_revision_for_ref(url: &str, reference: &str) -> Result<Revision>
|
||||
Ok(Revision(references.remove(0).revision))
|
||||
}
|
||||
|
||||
+/// Find the default branch for a git repository
|
||||
+pub fn find_default_branch(url: &str) -> Result<String> {
|
||||
+ let output = Command::new("git")
|
||||
+ .arg("ls-remote")
|
||||
+ .args(["--symref", url, "HEAD"])
|
||||
+ .output()
|
||||
+ .context("Failed to execute git ls-remote. Most likely it's not on PATH")?;
|
||||
+
|
||||
+ if !output.status.success() {
|
||||
+ bail!(
|
||||
+ "Failed to find the default branch for {}\n{}",
|
||||
+ url,
|
||||
+ String::from_utf8_lossy(&output.stderr)
|
||||
+ )
|
||||
+ }
|
||||
+
|
||||
+ let re = Regex::new(r"ref:.*refs/heads/(?<branch>.*)\tHEAD")?;
|
||||
+
|
||||
+ let Some(branch) = String::from_utf8_lossy(&output.stdout)
|
||||
+ .lines()
|
||||
+ .find_map(|x| re.captures(x).map(|matched| matched["branch"].into()))
|
||||
+ else {
|
||||
+ bail!("Failed to find the default branch for {url}",)
|
||||
+ };
|
||||
+
|
||||
+ Ok(branch)
|
||||
+}
|
||||
+
|
||||
/// Call `git ls-remote` with the provided args.
|
||||
fn ls_remote(args: &[&str]) -> Result<Vec<RemoteInfo>> {
|
||||
let output = Command::new("git")
|
||||
diff --git a/rust/lon/src/init/niv.rs b/rust/lon/src/init/niv.rs
|
||||
index 469fdc7..8d41670 100644
|
||||
--- a/rust/lon/src/init/niv.rs
|
||||
+++ b/rust/lon/src/init/niv.rs
|
||||
@@ -42,7 +42,7 @@ impl Convertible for LockFile {
|
||||
let source = GitHubSource::new(
|
||||
owner,
|
||||
&package.repo,
|
||||
- &package.branch,
|
||||
+ Some(&package.branch),
|
||||
Some(&package.rev),
|
||||
false,
|
||||
)?;
|
||||
@@ -51,7 +51,7 @@ impl Convertible for LockFile {
|
||||
} else {
|
||||
let source = GitSource::new(
|
||||
&package.repo,
|
||||
- &package.branch,
|
||||
+ Some(&package.branch),
|
||||
Some(&package.rev),
|
||||
false,
|
||||
false,
|
||||
diff --git a/rust/lon/src/sources.rs b/rust/lon/src/sources.rs
|
||||
index 92d8c2b..78bdbdb 100644
|
||||
--- a/rust/lon/src/sources.rs
|
||||
+++ b/rust/lon/src/sources.rs
|
||||
@@ -170,11 +170,16 @@ pub struct GitSource {
|
||||
impl GitSource {
|
||||
pub fn new(
|
||||
url: &str,
|
||||
- branch: &str,
|
||||
+ branch: Option<&String>,
|
||||
revision: Option<&String>,
|
||||
submodules: bool,
|
||||
frozen: bool,
|
||||
) -> Result<Self> {
|
||||
+ let branch = match branch {
|
||||
+ Some(branch) => branch,
|
||||
+ None => &git::find_default_branch(url)?,
|
||||
+ };
|
||||
+
|
||||
let rev = match revision {
|
||||
Some(rev) => rev,
|
||||
None => &git::find_newest_revision(url, branch)?.to_string(),
|
||||
@@ -283,13 +288,20 @@ impl GitHubSource {
|
||||
pub fn new(
|
||||
owner: &str,
|
||||
repo: &str,
|
||||
- branch: &str,
|
||||
+ branch: Option<&String>,
|
||||
revision: Option<&String>,
|
||||
frozen: bool,
|
||||
) -> Result<Self> {
|
||||
+ let repo_url = &Self::git_url(owner, repo);
|
||||
+
|
||||
+ let branch = match branch {
|
||||
+ Some(branch) => branch,
|
||||
+ None => &git::find_default_branch(repo_url)?,
|
||||
+ };
|
||||
+
|
||||
let rev = match revision {
|
||||
Some(rev) => rev,
|
||||
- None => &git::find_newest_revision(&Self::git_url(owner, repo), branch)?.to_string(),
|
||||
+ None => &git::find_newest_revision(repo_url, branch)?.to_string(),
|
||||
};
|
||||
log::info!("Locked revision: {rev}");
|
||||
|
||||
|
||||
From eee3871a246605a7ab60714bb193846160ac8e64 Mon Sep 17 00:00:00 2001
|
||||
From: Tom Hubrecht <tom@hubrecht.ovh>
|
||||
Date: Tue, 10 Jun 2025 17:25:52 +0200
|
||||
Subject: [PATCH 2/2] cli: init from npins
|
||||
|
||||
We convert three types of pins: `Git`, `GitRelease` and `Channel`
|
||||
---
|
||||
rust/lon/src/cli.rs | 13 ++-
|
||||
rust/lon/src/init.rs | 1 +
|
||||
rust/lon/src/init/npins.rs | 218 +++++++++++++++++++++++++++++++++++++
|
||||
rust/lon/tests/npins.json | 86 +++++++++++++++
|
||||
4 files changed, 312 insertions(+), 6 deletions(-)
|
||||
create mode 100644 rust/lon/src/init/npins.rs
|
||||
create mode 100644 rust/lon/tests/npins.json
|
||||
|
||||
diff --git a/rust/lon/src/cli.rs b/rust/lon/src/cli.rs
|
||||
index 5806b1d..57dcc50 100644
|
||||
--- a/rust/lon/src/cli.rs
|
||||
+++ b/rust/lon/src/cli.rs
|
||||
@@ -11,7 +11,7 @@ use crate::{
|
||||
bot::{Forge, Forgejo, GitHub, GitLab},
|
||||
commit_message::CommitMessage,
|
||||
git,
|
||||
- init::{Convertible, niv},
|
||||
+ init::{Convertible, niv, npins},
|
||||
lock::Lock,
|
||||
lon_nix::LonNix,
|
||||
sources::{GitHubSource, GitSource, Source, Sources},
|
||||
@@ -82,6 +82,7 @@ struct InitArgs {
|
||||
#[derive(Clone, ValueEnum)]
|
||||
enum LockFileType {
|
||||
Niv,
|
||||
+ Npins,
|
||||
}
|
||||
|
||||
#[derive(Subcommand)]
|
||||
@@ -261,13 +262,13 @@ fn init(directory: impl AsRef<Path>, args: &InitArgs) -> Result<()> {
|
||||
bail!("No lock file type is provided");
|
||||
};
|
||||
|
||||
- let lock_file = match lock_file_type {
|
||||
- LockFileType::Niv => niv::LockFile::from_file(path)?,
|
||||
- };
|
||||
-
|
||||
log::info!("Initializing lon.lock from {path:?}");
|
||||
|
||||
- let sources = lock_file.convert()?;
|
||||
+ let sources = match lock_file_type {
|
||||
+ LockFileType::Niv => niv::LockFile::from_file(path)?.convert()?,
|
||||
+ LockFileType::Npins => npins::LockFile::from_file(path)?.convert()?,
|
||||
+ };
|
||||
+
|
||||
sources.write(&directory)?;
|
||||
|
||||
Ok(())
|
||||
diff --git a/rust/lon/src/init.rs b/rust/lon/src/init.rs
|
||||
index ec87afa..06e63f2 100644
|
||||
--- a/rust/lon/src/init.rs
|
||||
+++ b/rust/lon/src/init.rs
|
||||
@@ -1,4 +1,5 @@
|
||||
pub mod niv;
|
||||
+pub mod npins;
|
||||
|
||||
use anyhow::Result;
|
||||
|
||||
diff --git a/rust/lon/src/init/npins.rs b/rust/lon/src/init/npins.rs
|
||||
new file mode 100644
|
||||
index 0000000..8a38139
|
||||
--- /dev/null
|
||||
+++ b/rust/lon/src/init/npins.rs
|
||||
@@ -0,0 +1,218 @@
|
||||
+use std::{collections::BTreeMap, fs::File, io::Read, path::Path};
|
||||
+
|
||||
+use anyhow::{Context, Result, bail};
|
||||
+use regex::Regex;
|
||||
+use serde::Deserialize;
|
||||
+
|
||||
+use crate::{
|
||||
+ init::Convertible,
|
||||
+ sources::{GitHubSource, GitSource, Source, Sources},
|
||||
+};
|
||||
+
|
||||
+#[derive(Debug, Deserialize)]
|
||||
+pub struct LockFile {
|
||||
+ pins: BTreeMap<String, Pin>,
|
||||
+ version: u64,
|
||||
+}
|
||||
+
|
||||
+#[derive(Debug, Deserialize)]
|
||||
+#[serde(tag = "type")]
|
||||
+pub enum Repository {
|
||||
+ Git {
|
||||
+ /// URL to the Git repository
|
||||
+ url: String,
|
||||
+ },
|
||||
+ Forgejo {
|
||||
+ server: String,
|
||||
+ owner: String,
|
||||
+ repo: String,
|
||||
+ },
|
||||
+ GitHub {
|
||||
+ /// "owner/repo"
|
||||
+ owner: String,
|
||||
+ repo: String,
|
||||
+ },
|
||||
+ GitLab {
|
||||
+ /// usually "owner/repo" or "group/owner/repo" (without leading or trailing slashes)
|
||||
+ repo_path: String,
|
||||
+ /// Of the kind <https://gitlab.example.org/>
|
||||
+ ///
|
||||
+ /// It must fit into the schema `<server>/<owner>/<repo>` to get a repository's URL.
|
||||
+ server: String,
|
||||
+ /// access token for private repositories
|
||||
+ #[serde(skip_serializing_if = "Option::is_none")]
|
||||
+ #[serde(default)]
|
||||
+ private_token: Option<String>,
|
||||
+ },
|
||||
+}
|
||||
+
|
||||
+// HACK: We know that a Git pin has a branch associated to it and GitRelease has none,
|
||||
+// but to unify the behaviour, we set them bot to `Option`s
|
||||
+#[derive(Debug, Deserialize)]
|
||||
+#[serde(tag = "type")]
|
||||
+pub enum Pin {
|
||||
+ Git {
|
||||
+ repository: Repository,
|
||||
+ branch: Option<String>,
|
||||
+ revision: String,
|
||||
+ submodules: bool,
|
||||
+ #[serde(default)]
|
||||
+ frozen: bool,
|
||||
+ },
|
||||
+ GitRelease {
|
||||
+ repository: Repository,
|
||||
+ branch: Option<String>,
|
||||
+ revision: String,
|
||||
+ submodules: bool,
|
||||
+ #[serde(default)]
|
||||
+ frozen: bool,
|
||||
+ },
|
||||
+ Channel {
|
||||
+ #[serde(rename = "name")]
|
||||
+ channel: String,
|
||||
+ url: String,
|
||||
+ #[serde(default)]
|
||||
+ frozen: bool,
|
||||
+ },
|
||||
+}
|
||||
+
|
||||
+impl LockFile {
|
||||
+ pub fn from_file(path: impl AsRef<Path>) -> Result<Self> {
|
||||
+ let file = File::open(path.as_ref())
|
||||
+ .with_context(|| format!("Failed to open {:?}", path.as_ref()))?;
|
||||
+ Self::from_reader(file)
|
||||
+ }
|
||||
+
|
||||
+ fn from_reader(rdr: impl Read) -> Result<Self> {
|
||||
+ serde_json::from_reader(rdr).context("Failed to deserialize npins lock file")
|
||||
+ }
|
||||
+}
|
||||
+
|
||||
+impl Convertible for LockFile {
|
||||
+ fn convert(&self) -> Result<Sources> {
|
||||
+ let mut sources = Sources::default();
|
||||
+
|
||||
+ if self.version == 1 {
|
||||
+ bail!("Unsupported npins lockfile version: {}", &self.version)
|
||||
+ }
|
||||
+
|
||||
+ let re = Regex::new(
|
||||
+ r"https://releases\.nixos\.org/.*\.(?<shortrev>[a-f0-9]+)/nixexprs\.tar\.xz",
|
||||
+ )?;
|
||||
+
|
||||
+ for (name, pin) in &self.pins {
|
||||
+ log::info!("Converting {name}...");
|
||||
+
|
||||
+ let source = match pin {
|
||||
+ Pin::Channel {
|
||||
+ channel,
|
||||
+ url,
|
||||
+ frozen,
|
||||
+ } => {
|
||||
+ let Some(matched) = re.captures(url) else {
|
||||
+ bail!("Cannot extract revision from the channel url: {url}")
|
||||
+ };
|
||||
+
|
||||
+ Source::GitHub(GitHubSource::new(
|
||||
+ "NixOS",
|
||||
+ "nixpkgs",
|
||||
+ Some(channel),
|
||||
+ Some(&matched["shortrev"].into()),
|
||||
+ *frozen,
|
||||
+ )?)
|
||||
+ }
|
||||
+ Pin::Git {
|
||||
+ repository,
|
||||
+ branch,
|
||||
+ revision,
|
||||
+ submodules,
|
||||
+ frozen,
|
||||
+ }
|
||||
+ | Pin::GitRelease {
|
||||
+ repository,
|
||||
+ branch,
|
||||
+ revision,
|
||||
+ submodules,
|
||||
+ frozen,
|
||||
+ } => match repository {
|
||||
+ Repository::Git { url } => Source::Git(GitSource::new(
|
||||
+ url,
|
||||
+ branch.as_ref(),
|
||||
+ Some(revision),
|
||||
+ *submodules,
|
||||
+ *frozen,
|
||||
+ )?),
|
||||
+ Repository::GitHub { owner, repo } => {
|
||||
+ if *submodules {
|
||||
+ Source::Git(GitSource::new(
|
||||
+ &format!("https://github.com/{owner}/{repo}"),
|
||||
+ branch.as_ref(),
|
||||
+ Some(revision),
|
||||
+ *submodules,
|
||||
+ *frozen,
|
||||
+ )?)
|
||||
+ } else {
|
||||
+ Source::GitHub(GitHubSource::new(
|
||||
+ owner,
|
||||
+ repo,
|
||||
+ branch.as_ref(),
|
||||
+ Some(revision),
|
||||
+ *frozen,
|
||||
+ )?)
|
||||
+ }
|
||||
+ }
|
||||
+ Repository::Forgejo {
|
||||
+ server,
|
||||
+ owner,
|
||||
+ repo,
|
||||
+ } => Source::Git(GitSource::new(
|
||||
+ &format!("{server}/{owner}/{repo}"),
|
||||
+ branch.as_ref(),
|
||||
+ Some(revision),
|
||||
+ *submodules,
|
||||
+ *frozen,
|
||||
+ )?),
|
||||
+ Repository::GitLab {
|
||||
+ repo_path,
|
||||
+ server,
|
||||
+ private_token,
|
||||
+ } => {
|
||||
+ if private_token.is_some() {
|
||||
+ log::warn!(
|
||||
+ "GitLab source {name} is configured with a PAT, which unsupported in lon"
|
||||
+ );
|
||||
+ }
|
||||
+ Source::Git(GitSource::new(
|
||||
+ &format!("{server}/{repo_path}"),
|
||||
+ branch.as_ref(),
|
||||
+ Some(revision),
|
||||
+ *submodules,
|
||||
+ *frozen,
|
||||
+ )?)
|
||||
+ }
|
||||
+ },
|
||||
+ };
|
||||
+
|
||||
+ sources.add(name, source);
|
||||
+ }
|
||||
+
|
||||
+ Ok(sources)
|
||||
+ }
|
||||
+}
|
||||
+
|
||||
+#[cfg(test)]
|
||||
+mod tests {
|
||||
+ use super::*;
|
||||
+
|
||||
+ impl LockFile {
|
||||
+ fn from_str(s: &str) -> Result<Self> {
|
||||
+ serde_json::from_str(s).context("Failed to deserialize npins lock file")
|
||||
+ }
|
||||
+ }
|
||||
+
|
||||
+ #[test]
|
||||
+ fn parse_npins_lock_file() -> Result<()> {
|
||||
+ LockFile::from_str(include_str!("../../tests/npins.json"))?;
|
||||
+ Ok(())
|
||||
+ }
|
||||
+}
|
||||
diff --git a/rust/lon/tests/npins.json b/rust/lon/tests/npins.json
|
||||
new file mode 100644
|
||||
index 0000000..10ce4e2
|
||||
--- /dev/null
|
||||
+++ b/rust/lon/tests/npins.json
|
||||
@@ -0,0 +1,86 @@
|
||||
+{
|
||||
+ "pins": {
|
||||
+ "agenix": {
|
||||
+ "type": "GitRelease",
|
||||
+ "repository": {
|
||||
+ "type": "GitHub",
|
||||
+ "owner": "ryantm",
|
||||
+ "repo": "agenix"
|
||||
+ },
|
||||
+ "pre_releases": false,
|
||||
+ "version_upper_bound": null,
|
||||
+ "release_prefix": null,
|
||||
+ "submodules": false,
|
||||
+ "version": "0.15.0",
|
||||
+ "revision": "564595d0ad4be7277e07fa63b5a991b3c645655d",
|
||||
+ "url": "https://api.github.com/repos/ryantm/agenix/tarball/refs/tags/0.15.0",
|
||||
+ "hash": "sha256-ipqShkBmHKC9ft1ZAsA6aeKps32k7+XZSPwfxeHLsAU="
|
||||
+ },
|
||||
+ "arkheon": {
|
||||
+ "type": "Git",
|
||||
+ "repository": {
|
||||
+ "type": "GitHub",
|
||||
+ "owner": "RaitoBezarius",
|
||||
+ "repo": "arkheon"
|
||||
+ },
|
||||
+ "branch": "main",
|
||||
+ "submodules": false,
|
||||
+ "revision": "3eea876b29217d01cf2ef03ea9fdd8779d28ad04",
|
||||
+ "url": "https://github.com/RaitoBezarius/arkheon/archive/3eea876b29217d01cf2ef03ea9fdd8779d28ad04.tar.gz",
|
||||
+ "hash": "sha256-+R6MhTXuSzNeGQiL4DQwlP5yNhmnhbf7pQWPUWgcZSM="
|
||||
+ },
|
||||
+ "colmena": {
|
||||
+ "type": "Git",
|
||||
+ "repository": {
|
||||
+ "type": "Git",
|
||||
+ "url": "https://git.dgnum.eu/DGNum/colmena"
|
||||
+ },
|
||||
+ "branch": "main",
|
||||
+ "submodules": false,
|
||||
+ "revision": "b5135dc8af1d7637b337cc2632990400221da577",
|
||||
+ "url": null,
|
||||
+ "hash": "sha256-7gg+K3PEYlN0sGPgDlmnM8zgDDIV505gNcwjFN61Qvk="
|
||||
+ },
|
||||
+ "nix-actions": {
|
||||
+ "type": "GitRelease",
|
||||
+ "repository": {
|
||||
+ "type": "Git",
|
||||
+ "url": "https://git.dgnum.eu/DGNum/nix-actions.git"
|
||||
+ },
|
||||
+ "pre_releases": false,
|
||||
+ "version_upper_bound": null,
|
||||
+ "release_prefix": null,
|
||||
+ "submodules": false,
|
||||
+ "version": "v0.5.1",
|
||||
+ "revision": "06847b3256df402da0475dccb290832ec92a9f8c",
|
||||
+ "url": null,
|
||||
+ "hash": "sha256-2xOZdKiUfcriQFKG37vY96dgCJLndhLa7cGacq8+SA8="
|
||||
+ },
|
||||
+ "nixos-25.05": {
|
||||
+ "type": "Channel",
|
||||
+ "name": "nixos-25.05",
|
||||
+ "url": "https://releases.nixos.org/nixos/25.05/nixos-25.05.803579.70c74b02eac4/nixexprs.tar.xz",
|
||||
+ "hash": "sha256-0RxtgAd4gHYPFFwICal8k8hvJBOkCeTjFkh4HsqYDbE="
|
||||
+ },
|
||||
+ "nixos-unstable": {
|
||||
+ "type": "Channel",
|
||||
+ "name": "nixos-unstable",
|
||||
+ "url": "https://releases.nixos.org/nixos/unstable/nixos-25.05pre797896.d89fc19e405c/nixexprs.tar.xz",
|
||||
+ "hash": "sha256-bFJJ/qwB3VJ0nFuVYYHJXinT4tNJ2jhXTVT6SpYiFOM="
|
||||
+ },
|
||||
+ "wp4nix": {
|
||||
+ "type": "Git",
|
||||
+ "repository": {
|
||||
+ "type": "GitLab",
|
||||
+ "repo_path": "helsinki-systems/wp4nix",
|
||||
+ "server": "https://git.helsinki.tools/"
|
||||
+ },
|
||||
+ "branch": "master",
|
||||
+ "submodules": false,
|
||||
+ "revision": "2fc9a0734168cab536e3129efa6397d6cd3ac89f",
|
||||
+ "url": "https://git.helsinki.tools/api/v4/projects/helsinki-systems%2Fwp4nix/repository/archive.tar.gz?sha=2fc9a0734168cab536e3129efa6397d6cd3ac89f",
|
||||
+ "hash": "sha256-abwqAZGsWuWqfxou8XlqedBvXsUw1/xanSgljLCJxdM="
|
||||
+ }
|
||||
+ },
|
||||
+ "version": 6
|
||||
+}
|
3
patches/lon/01-npins-import.patch.license
Normal file
3
patches/lon/01-npins-import.patch.license
Normal file
|
@ -0,0 +1,3 @@
|
|||
SPDX-FileCopyrightText: 2025 Tom Hubrecht <tom.hubrecht@dgnum.eu>
|
||||
|
||||
SPDX-License-Identifier: MIT
|
Loading…
Add table
Add a link
Reference in a new issue