init
This commit is contained in:
commit
807c3b0094
11 changed files with 533 additions and 0 deletions
80
README.md
Normal file
80
README.md
Normal 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
13
example-config.json
Normal 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
27
flake.lock
generated
Normal 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
70
flake.nix
Normal 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
21
meson.build
Normal 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
40
module.nix
Normal 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
99
src/config.cc
Normal 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
35
src/config.hh
Normal 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
8
src/plugin.cc
Normal 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
97
src/shorturl-scheme.cc
Normal 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
43
src/shorturl-scheme.hh
Normal 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
|
||||
Loading…
Add table
Add a link
Reference in a new issue