From 807c3b0094963bb78198643b62b78e2a0c916230 Mon Sep 17 00:00:00 2001 From: atagen Date: Wed, 25 Feb 2026 13:16:02 +1100 Subject: [PATCH] init --- README.md | 80 ++++++++++++++++++++++++++++++++++ example-config.json | 13 ++++++ flake.lock | 27 ++++++++++++ flake.nix | 70 +++++++++++++++++++++++++++++ meson.build | 21 +++++++++ module.nix | 40 +++++++++++++++++ src/config.cc | 99 ++++++++++++++++++++++++++++++++++++++++++ src/config.hh | 35 +++++++++++++++ src/plugin.cc | 8 ++++ src/shorturl-scheme.cc | 97 +++++++++++++++++++++++++++++++++++++++++ src/shorturl-scheme.hh | 43 ++++++++++++++++++ 11 files changed, 533 insertions(+) create mode 100644 README.md create mode 100644 example-config.json create mode 100644 flake.lock create mode 100644 flake.nix create mode 100644 meson.build create mode 100644 module.nix create mode 100644 src/config.cc create mode 100644 src/config.hh create mode 100644 src/plugin.cc create mode 100644 src/shorturl-scheme.cc create mode 100644 src/shorturl-scheme.hh diff --git a/README.md b/README.md new file mode 100644 index 0000000..c7e6457 --- /dev/null +++ b/README.md @@ -0,0 +1,80 @@ +# nix-shorturl-plugin + +A Nix plugin that lets you define custom short URL schemes for flake references. + +For example, `myorg:mylib` can expand to `github:my-organization/mylib`. + +## How it works + +The plugin registers a custom `InputScheme` that intercepts URL resolution. When Nix encounters a URL like `myorg:mylib`, the plugin: + +1. Matches `myorg` against your configured schemes +2. Expands the template (e.g., `github:my-organization/{path}` becomes `github:my-organization/mylib`) +3. Delegates to the built-in scheme (e.g., the GitHub fetcher) + +Lock files store the **real** resolved type (e.g., `type = "github"`), not `type = "shorturl"`. This means lock files work on machines that don't have the plugin installed. + +## Installation + +```bash +nix build github:yourusername/nix-shorturl-plugin +``` + +Then add to your Nix configuration (`~/.config/nix/nix.conf`): + +``` +plugin-files = /path/to/result/lib/nix/plugins/libnix-shorturl-plugin.so +``` + +Or use it ad-hoc: + +```bash +nix --option plugin-files ./result/lib/nix/plugins/libnix-shorturl-plugin.so flake metadata myorg:mylib +``` + +## Configuration + +Create `~/.config/nix/shorturl.json` (or set `NIX_SHORTURL_CONFIG`): + +```json +{ + "schemes": { + "myorg": { + "template": "github:my-organization/{path}" + }, + "internal": { + "template": "git+ssh://git.internal.corp/{path}" + }, + "gl": { + "template": "gitlab:mycompany/{path}" + } + } +} +``` + +### Template syntax + +- `{path}` is replaced with everything after the colon in the short URL +- Query parameters and fragments from the original URL are passed through + +### Examples + +| Short URL | Expanded URL | +|---|---| +| `myorg:mylib` | `github:my-organization/mylib` | +| `myorg:libs/mylib?ref=develop` | `github:my-organization/libs/mylib?ref=develop` | +| `gl:project` | `gitlab:mycompany/project` | + +## Config path resolution + +1. `NIX_SHORTURL_CONFIG` environment variable +2. `$XDG_CONFIG_HOME/nix/shorturl.json` +3. `~/.config/nix/shorturl.json` + +## Development + +```bash +nix develop +meson setup build +ninja -C build +``` diff --git a/example-config.json b/example-config.json new file mode 100644 index 0000000..f989b6b --- /dev/null +++ b/example-config.json @@ -0,0 +1,13 @@ +{ + "schemes": { + "myorg": { + "template": "github:my-organization/{path}" + }, + "internal": { + "template": "git+ssh://git.internal.corp/{path}" + }, + "gl": { + "template": "gitlab:mycompany/{path}" + } + } +} diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..f52c5ed --- /dev/null +++ b/flake.lock @@ -0,0 +1,27 @@ +{ + "nodes": { + "nixpkgs": { + "locked": { + "lastModified": 1771848320, + "narHash": "sha256-0MAd+0mun3K/Ns8JATeHT1sX28faLII5hVLq0L3BdZU=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "2fc6539b481e1d2569f25f8799236694180c0993", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "nixpkgs": "nixpkgs" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..8a726af --- /dev/null +++ b/flake.nix @@ -0,0 +1,70 @@ +{ + description = "User-defined short URL schemes for Nix flake references"; + + inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; + + outputs = + { self, nixpkgs }: + let + forAllSystems = nixpkgs.lib.genAttrs [ + "x86_64-linux" + "aarch64-linux" + "x86_64-darwin" + "aarch64-darwin" + ]; + in + { + nixosModules.default = + { pkgs, ... }: + { + imports = [ ./module.nix ]; + nix.shorturls.package = self.packages.${pkgs.stdenv.hostPlatform.system}.default; + }; + + packages = forAllSystems ( + system: + let + pkgs = nixpkgs.legacyPackages.${system}; + in + { + default = pkgs.stdenv.mkDerivation { + pname = "nix-shorturl-plugin"; + version = "0.1.0"; + src = self; + + nativeBuildInputs = with pkgs; [ + meson + ninja + pkg-config + ]; + + buildInputs = with pkgs; [ + nix.dev + nlohmann_json + boost + ]; + }; + } + ); + + devShells = forAllSystems ( + system: + let + pkgs = nixpkgs.legacyPackages.${system}; + in + { + default = pkgs.mkShell { + packages = with pkgs; [ + meson + ninja + pkg-config + nix.dev + nlohmann_json + boost + clang-tools + ]; + }; + } + ); + }; +} diff --git a/meson.build b/meson.build new file mode 100644 index 0000000..97ad2e4 --- /dev/null +++ b/meson.build @@ -0,0 +1,21 @@ +project('nix-shorturl-plugin', 'cpp', + version : '0.1.0', + default_options : [ + 'cpp_std=c++23', + 'warning_level=2', + ], +) + +nix_fetchers = dependency('nix-fetchers') +nix_util = dependency('nix-util') +nix_store = dependency('nix-store') +nlohmann_json = dependency('nlohmann_json', version : '>=3.9') + +plugin = shared_library('nix-shorturl-plugin', + 'src/plugin.cc', + 'src/shorturl-scheme.cc', + 'src/config.cc', + dependencies : [nix_fetchers, nix_util, nix_store, nlohmann_json], + install : true, + install_dir : get_option('libdir') / 'nix' / 'plugins', +) diff --git a/module.nix b/module.nix new file mode 100644 index 0000000..39e7248 --- /dev/null +++ b/module.nix @@ -0,0 +1,40 @@ +{ config, lib, pkgs, ... }: + +let + cfg = config.nix.shorturls; + + configFile = pkgs.writeText "shorturl.json" (builtins.toJSON { + schemes = lib.mapAttrs (_: v: { template = v; }) cfg.schemes; + }); +in { + options.nix.shorturls = { + enable = lib.mkEnableOption "nix-shorturl-plugin, user-defined short URL schemes for Nix flake references"; + + package = lib.mkPackageOption pkgs "nix-shorturl-plugin" { + default = null; + }; + + schemes = lib.mkOption { + type = lib.types.attrsOf lib.types.str; + default = {}; + example = lib.literalExpression '' + { + myorg = "github:my-organization/{path}"; + internal = "git+ssh://git.internal.corp/{path}"; + gl = "gitlab:mycompany/{path}"; + } + ''; + description = '' + Attribute set of short URL schemes. Each key is the scheme name + and the value is the URL template. Use `{path}` as a placeholder + for the path component of the short URL. + ''; + }; + }; + + config = lib.mkIf cfg.enable { + nix.settings.plugin-files = [ "${cfg.package}/lib/nix/plugins/libnix-shorturl-plugin.so" ]; + + environment.variables.NIX_SHORTURL_CONFIG = toString configFile; + }; +} diff --git a/src/config.cc b/src/config.cc new file mode 100644 index 0000000..6e330de --- /dev/null +++ b/src/config.cc @@ -0,0 +1,99 @@ +#include "config.hh" + +#include +#include +#include + +namespace nix::shorturl { + +std::filesystem::path Config::defaultConfigPath() +{ + if (auto env = std::getenv("NIX_SHORTURL_CONFIG")) + return env; + + if (auto xdg = std::getenv("XDG_CONFIG_HOME")) + return std::filesystem::path(xdg) / "nix" / "shorturl.json"; + + if (auto home = std::getenv("HOME")) + return std::filesystem::path(home) / ".config" / "nix" / "shorturl.json"; + + return {}; +} + +std::optional Config::load(const std::filesystem::path & path) +{ + if (path.empty() || !std::filesystem::exists(path)) + return std::nullopt; + + std::ifstream file(path); + if (!file.is_open()) + return std::nullopt; + + nlohmann::json json; + try { + file >> json; + } catch (const nlohmann::json::parse_error &) { + return std::nullopt; + } + + Config config; + + if (!json.contains("schemes") || !json["schemes"].is_object()) + return std::nullopt; + + for (auto & [name, value] : json["schemes"].items()) { + if (!value.is_object() || !value.contains("template") || !value["template"].is_string()) + continue; + config.schemes[name] = SchemeConfig{value["template"].get()}; + } + + return config; +} + +std::optional Config::load() +{ + return load(defaultConfigPath()); +} + +bool Config::hasScheme(const std::string & scheme) const +{ + return schemes.count(scheme) > 0; +} + +std::optional Config::expand( + const std::string & scheme, + const std::string & path, + const std::map> & query, + const std::string & fragment) const +{ + auto it = schemes.find(scheme); + if (it == schemes.end()) + return std::nullopt; + + std::string result = it->second.templateStr; + + // Replace {path} with the actual path + std::string placeholder = "{path}"; + auto pos = result.find(placeholder); + if (pos != std::string::npos) + result.replace(pos, placeholder.size(), path); + + // Append query parameters + if (!query.empty()) { + bool first = (result.find('?') == std::string::npos); + for (auto & [k, v] : query) { + result += first ? '?' : '&'; + result += k + "=" + v; + first = false; + } + } + + // Append fragment + if (!fragment.empty()) { + result += "#" + fragment; + } + + return result; +} + +} // namespace nix::shorturl diff --git a/src/config.hh b/src/config.hh new file mode 100644 index 0000000..27367a7 --- /dev/null +++ b/src/config.hh @@ -0,0 +1,35 @@ +#pragma once + +#include +#include +#include +#include +#include + +namespace nix::shorturl { + +struct SchemeConfig { + std::string templateStr; +}; + +struct Config { + std::map schemes; + + static std::filesystem::path defaultConfigPath(); + static std::optional load(const std::filesystem::path & path); + static std::optional load(); + + /** + * Expand a short URL scheme + path into a full URL string. + * Query parameters and fragment are appended to the expanded URL. + */ + std::optional expand( + const std::string & scheme, + const std::string & path, + const std::map> & query = {}, + const std::string & fragment = {}) const; + + bool hasScheme(const std::string & scheme) const; +}; + +} // namespace nix::shorturl diff --git a/src/plugin.cc b/src/plugin.cc new file mode 100644 index 0000000..c8eaa43 --- /dev/null +++ b/src/plugin.cc @@ -0,0 +1,8 @@ +#include "shorturl-scheme.hh" + +#include + +static auto reg = nix::OnStartup([] { + nix::fetchers::registerInputScheme( + std::make_shared()); +}); diff --git a/src/shorturl-scheme.cc b/src/shorturl-scheme.cc new file mode 100644 index 0000000..a55abcd --- /dev/null +++ b/src/shorturl-scheme.cc @@ -0,0 +1,97 @@ +#include "shorturl-scheme.hh" + +#include +#include + +namespace nix::shorturl { + +ShortUrlInputScheme::ShortUrlInputScheme() +{ + if (auto cfg = Config::load()) + config = std::move(*cfg); +} + +std::string_view ShortUrlInputScheme::schemeName() const +{ + return "shorturl"; +} + +std::optional +ShortUrlInputScheme::inputFromURL( + const nix::fetchers::Settings & settings, + const ParsedURL & url, + bool requireTree) const +{ + if (!config.hasScheme(url.scheme)) + return std::nullopt; + + auto expanded = config.expand(url.scheme, url.path, url.query, url.fragment); + if (!expanded) + return std::nullopt; + + // Parse expanded URL and check it doesn't resolve to another shorturl scheme + auto parsedExpanded = nix::parseURL(*expanded); + if (config.hasScheme(parsedExpanded.scheme)) + throw nix::Error("shorturl scheme '%s' expands to another shorturl scheme '%s' (circular reference)", + url.scheme, parsedExpanded.scheme); + + // Delegate to the real scheme via Input::fromURL + return nix::fetchers::Input::fromURL(settings, *expanded, requireTree); +} + +std::optional +ShortUrlInputScheme::inputFromAttrs( + const nix::fetchers::Settings & /* settings */, + const nix::fetchers::Attrs & /* attrs */) const +{ + // Lock files store the real type (e.g., "github"), not "shorturl". + // We never match from attrs — this ensures lock files work without the plugin. + return std::nullopt; +} + +StringSet ShortUrlInputScheme::allowedAttrs() const +{ + return {}; +} + +std::pair, nix::fetchers::Input> +ShortUrlInputScheme::getAccessor(nix::ref store, const nix::fetchers::Input & input) const +{ + // Reconstruct the real Input from the stored attrs (which have the real type). + // Call the real scheme's getAccessor directly to avoid double fingerprint assignment + // in Input::getAccessorUnchecked. + auto attrs = input.toAttrs(); + auto realInput = nix::fetchers::Input::fromAttrs(*input.settings, std::move(attrs)); + return realInput.scheme->getAccessor(store, realInput); +} + +bool ShortUrlInputScheme::isLocked(const nix::fetchers::Input & input) const +{ + auto attrs = input.toAttrs(); + auto realInput = nix::fetchers::Input::fromAttrs(*input.settings, std::move(attrs)); + return realInput.isLocked(); +} + +std::optional +ShortUrlInputScheme::getFingerprint(nix::ref store, const nix::fetchers::Input & input) const +{ + auto attrs = input.toAttrs(); + auto realInput = nix::fetchers::Input::fromAttrs(*input.settings, std::move(attrs)); + return realInput.getFingerprint(store); +} + +ParsedURL ShortUrlInputScheme::toURL(const nix::fetchers::Input & input) const +{ + auto attrs = input.toAttrs(); + auto realInput = nix::fetchers::Input::fromAttrs(*input.settings, std::move(attrs)); + return realInput.toURL(); +} + +bool ShortUrlInputScheme::isDirect(const nix::fetchers::Input & input) const +{ + auto attrs = input.toAttrs(); + auto realInput = nix::fetchers::Input::fromAttrs(*input.settings, std::move(attrs)); + return realInput.isDirect(); +} + +} // namespace nix::shorturl diff --git a/src/shorturl-scheme.hh b/src/shorturl-scheme.hh new file mode 100644 index 0000000..420869b --- /dev/null +++ b/src/shorturl-scheme.hh @@ -0,0 +1,43 @@ +#pragma once + +#include "config.hh" + +#include + +namespace nix::shorturl { + +struct ShortUrlInputScheme : nix::fetchers::InputScheme +{ + Config config; + + ShortUrlInputScheme(); + + std::string_view schemeName() const override; + + std::optional + inputFromURL( + const nix::fetchers::Settings & settings, + const ParsedURL & url, + bool requireTree) const override; + + std::optional + inputFromAttrs( + const nix::fetchers::Settings & settings, + const nix::fetchers::Attrs & attrs) const override; + + StringSet allowedAttrs() const override; + + std::pair, nix::fetchers::Input> + getAccessor(nix::ref store, const nix::fetchers::Input & input) const override; + + bool isLocked(const nix::fetchers::Input & input) const override; + + std::optional + getFingerprint(nix::ref store, const nix::fetchers::Input & input) const override; + + ParsedURL toURL(const nix::fetchers::Input & input) const override; + + bool isDirect(const nix::fetchers::Input & input) const override; +}; + +} // namespace nix::shorturl