This commit is contained in:
atagen 2026-02-25 13:16:02 +11:00
commit 807c3b0094
11 changed files with 533 additions and 0 deletions

80
README.md Normal file
View file

@ -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
```

13
example-config.json Normal file
View file

@ -0,0 +1,13 @@
{
"schemes": {
"myorg": {
"template": "github:my-organization/{path}"
},
"internal": {
"template": "git+ssh://git.internal.corp/{path}"
},
"gl": {
"template": "gitlab:mycompany/{path}"
}
}
}

27
flake.lock generated Normal file
View file

@ -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
}

70
flake.nix Normal file
View file

@ -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
];
};
}
);
};
}

21
meson.build Normal file
View file

@ -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',
)

40
module.nix Normal file
View file

@ -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;
};
}

99
src/config.cc Normal file
View file

@ -0,0 +1,99 @@
#include "config.hh"
#include <cstdlib>
#include <fstream>
#include <nlohmann/json.hpp>
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> 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<std::string>()};
}
return config;
}
std::optional<Config> Config::load()
{
return load(defaultConfigPath());
}
bool Config::hasScheme(const std::string & scheme) const
{
return schemes.count(scheme) > 0;
}
std::optional<std::string> Config::expand(
const std::string & scheme,
const std::string & path,
const std::map<std::string, std::string, std::less<>> & 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

35
src/config.hh Normal file
View file

@ -0,0 +1,35 @@
#pragma once
#include <filesystem>
#include <functional>
#include <map>
#include <optional>
#include <string>
namespace nix::shorturl {
struct SchemeConfig {
std::string templateStr;
};
struct Config {
std::map<std::string, SchemeConfig> schemes;
static std::filesystem::path defaultConfigPath();
static std::optional<Config> load(const std::filesystem::path & path);
static std::optional<Config> load();
/**
* Expand a short URL scheme + path into a full URL string.
* Query parameters and fragment are appended to the expanded URL.
*/
std::optional<std::string> expand(
const std::string & scheme,
const std::string & path,
const std::map<std::string, std::string, std::less<>> & query = {},
const std::string & fragment = {}) const;
bool hasScheme(const std::string & scheme) const;
};
} // namespace nix::shorturl

8
src/plugin.cc Normal file
View file

@ -0,0 +1,8 @@
#include "shorturl-scheme.hh"
#include <nix/util/types.hh>
static auto reg = nix::OnStartup([] {
nix::fetchers::registerInputScheme(
std::make_shared<nix::shorturl::ShortUrlInputScheme>());
});

97
src/shorturl-scheme.cc Normal file
View file

@ -0,0 +1,97 @@
#include "shorturl-scheme.hh"
#include <nix/fetchers/attrs.hh>
#include <nix/util/url.hh>
namespace nix::shorturl {
ShortUrlInputScheme::ShortUrlInputScheme()
{
if (auto cfg = Config::load())
config = std::move(*cfg);
}
std::string_view ShortUrlInputScheme::schemeName() const
{
return "shorturl";
}
std::optional<nix::fetchers::Input>
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<nix::fetchers::Input>
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::ref<SourceAccessor>, nix::fetchers::Input>
ShortUrlInputScheme::getAccessor(nix::ref<Store> 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<std::string>
ShortUrlInputScheme::getFingerprint(nix::ref<Store> 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

43
src/shorturl-scheme.hh Normal file
View file

@ -0,0 +1,43 @@
#pragma once
#include "config.hh"
#include <nix/fetchers/fetchers.hh>
namespace nix::shorturl {
struct ShortUrlInputScheme : nix::fetchers::InputScheme
{
Config config;
ShortUrlInputScheme();
std::string_view schemeName() const override;
std::optional<nix::fetchers::Input>
inputFromURL(
const nix::fetchers::Settings & settings,
const ParsedURL & url,
bool requireTree) const override;
std::optional<nix::fetchers::Input>
inputFromAttrs(
const nix::fetchers::Settings & settings,
const nix::fetchers::Attrs & attrs) const override;
StringSet allowedAttrs() const override;
std::pair<nix::ref<SourceAccessor>, nix::fetchers::Input>
getAccessor(nix::ref<Store> store, const nix::fetchers::Input & input) const override;
bool isLocked(const nix::fetchers::Input & input) const override;
std::optional<std::string>
getFingerprint(nix::ref<Store> 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