mirror of
https://github.com/NixOS/nix.git
synced 2025-12-24 09:50:55 +01:00
Merge branch 'master' into document-derivation-and-deriving-path
This commit is contained in:
commit
f003b8ca92
378 changed files with 8891 additions and 6157 deletions
|
|
@ -5,7 +5,15 @@ in
|
|||
|
||||
builtinsInfo:
|
||||
let
|
||||
showBuiltin = name: { doc, type ? null, args ? [ ], experimental-feature ? null, impure-only ? false }:
|
||||
showBuiltin =
|
||||
name:
|
||||
{
|
||||
doc,
|
||||
type ? null,
|
||||
args ? [ ],
|
||||
experimental-feature ? null,
|
||||
impure-only ? false,
|
||||
}:
|
||||
let
|
||||
type' = optionalString (type != null) " (${type})";
|
||||
|
||||
|
|
|
|||
|
|
@ -32,7 +32,13 @@ let
|
|||
|
||||
commandInfo = fromJSON commandDump;
|
||||
|
||||
showCommand = { command, details, filename, toplevel }:
|
||||
showCommand =
|
||||
{
|
||||
command,
|
||||
details,
|
||||
filename,
|
||||
toplevel,
|
||||
}:
|
||||
let
|
||||
|
||||
result = ''
|
||||
|
|
@ -56,26 +62,27 @@ let
|
|||
${maybeOptions}
|
||||
'';
|
||||
|
||||
showSynopsis = command: args:
|
||||
showSynopsis =
|
||||
command: args:
|
||||
let
|
||||
showArgument = arg: "*${arg.label}*" + optionalString (! arg ? arity) "...";
|
||||
showArgument = arg: "*${arg.label}*" + optionalString (!arg ? arity) "...";
|
||||
arguments = concatStringsSep " " (map showArgument args);
|
||||
in ''
|
||||
in
|
||||
''
|
||||
`${command}` [*option*...] ${arguments}
|
||||
'';
|
||||
|
||||
maybeSubcommands = optionalString (details ? commands && details.commands != {})
|
||||
''
|
||||
where *subcommand* is one of the following:
|
||||
maybeSubcommands = optionalString (details ? commands && details.commands != { }) ''
|
||||
where *subcommand* is one of the following:
|
||||
|
||||
${subcommands}
|
||||
'';
|
||||
${subcommands}
|
||||
'';
|
||||
|
||||
subcommands = if length categories > 1
|
||||
then listCategories
|
||||
else listSubcommands details.commands;
|
||||
subcommands = if length categories > 1 then listCategories else listSubcommands details.commands;
|
||||
|
||||
categories = sort (x: y: x.id < y.id) (unique (map (cmd: cmd.category) (attrValues details.commands)));
|
||||
categories = sort (x: y: x.id < y.id) (
|
||||
unique (map (cmd: cmd.category) (attrValues details.commands))
|
||||
);
|
||||
|
||||
listCategories = concatStrings (map showCategory categories);
|
||||
|
||||
|
|
@ -99,38 +106,39 @@ let
|
|||
|
||||
${allStores}
|
||||
'';
|
||||
index = replaceStrings
|
||||
[ "@store-types@" "./local-store.md" "./local-daemon-store.md" ]
|
||||
[ storesOverview "#local-store" "#local-daemon-store" ]
|
||||
details.doc;
|
||||
index =
|
||||
replaceStrings
|
||||
[ "@store-types@" "./local-store.md" "./local-daemon-store.md" ]
|
||||
[ storesOverview "#local-store" "#local-daemon-store" ]
|
||||
details.doc;
|
||||
storesOverview =
|
||||
let
|
||||
showEntry = store:
|
||||
"- [${store.name}](#${store.slug})";
|
||||
showEntry = store: "- [${store.name}](#${store.slug})";
|
||||
in
|
||||
concatStringsSep "\n" (map showEntry storesList) + "\n";
|
||||
allStores = concatStringsSep "\n" (attrValues storePages);
|
||||
storePages = listToAttrs
|
||||
(map (s: { name = s.filename; value = s.page; }) storesList);
|
||||
storePages = listToAttrs (
|
||||
map (s: {
|
||||
name = s.filename;
|
||||
value = s.page;
|
||||
}) storesList
|
||||
);
|
||||
storesList = showStoreDocs {
|
||||
storeInfo = commandInfo.stores;
|
||||
inherit inlineHTML;
|
||||
};
|
||||
hasInfix = infix: content:
|
||||
hasInfix =
|
||||
infix: content:
|
||||
builtins.stringLength content != builtins.stringLength (replaceStrings [ infix ] [ "" ] content);
|
||||
in
|
||||
optionalString (details ? doc) (
|
||||
# An alternate implementation with builtins.match stack overflowed on some systems.
|
||||
if hasInfix "@store-types@" details.doc
|
||||
then help-stores
|
||||
else details.doc
|
||||
if hasInfix "@store-types@" details.doc then help-stores else details.doc
|
||||
);
|
||||
|
||||
maybeOptions =
|
||||
let
|
||||
allVisibleOptions = filterAttrs
|
||||
(_: o: ! o.hiddenCategory)
|
||||
(details.flags // toplevel.flags);
|
||||
allVisibleOptions = filterAttrs (_: o: !o.hiddenCategory) (details.flags // toplevel.flags);
|
||||
in
|
||||
optionalString (allVisibleOptions != { }) ''
|
||||
# Options
|
||||
|
|
@ -142,55 +150,73 @@ let
|
|||
> See [`man nix.conf`](@docroot@/command-ref/conf-file.md#command-line-flags) for overriding configuration settings with command line flags.
|
||||
'';
|
||||
|
||||
showOptions = inlineHTML: allOptions:
|
||||
showOptions =
|
||||
inlineHTML: allOptions:
|
||||
let
|
||||
showCategory = cat: opts: ''
|
||||
${optionalString (cat != "") "## ${cat}"}
|
||||
|
||||
${concatStringsSep "\n" (attrValues (mapAttrs showOption opts))}
|
||||
'';
|
||||
showOption = name: option:
|
||||
showOption =
|
||||
name: option:
|
||||
let
|
||||
result = trim ''
|
||||
- ${item}
|
||||
|
||||
${option.description}
|
||||
'';
|
||||
item = if inlineHTML
|
||||
then ''<span id="opt-${name}">[`--${name}`](#opt-${name})</span> ${shortName} ${labels}''
|
||||
else "`--${name}` ${shortName} ${labels}";
|
||||
shortName = optionalString
|
||||
(option ? shortName)
|
||||
("/ `-${option.shortName}`");
|
||||
labels = optionalString
|
||||
(option ? labels)
|
||||
(concatStringsSep " " (map (s: "*${s}*") option.labels));
|
||||
in result;
|
||||
categories = mapAttrs
|
||||
# Convert each group from a list of key-value pairs back to an attrset
|
||||
(_: listToAttrs)
|
||||
(groupBy
|
||||
(cmd: cmd.value.category)
|
||||
(attrsToList allOptions));
|
||||
in concatStrings (attrValues (mapAttrs showCategory categories));
|
||||
in squash result;
|
||||
item =
|
||||
if inlineHTML then
|
||||
''<span id="opt-${name}">[`--${name}`](#opt-${name})</span> ${shortName} ${labels}''
|
||||
else
|
||||
"`--${name}` ${shortName} ${labels}";
|
||||
shortName = optionalString (option ? shortName) ("/ `-${option.shortName}`");
|
||||
labels = optionalString (option ? labels) (concatStringsSep " " (map (s: "*${s}*") option.labels));
|
||||
in
|
||||
result;
|
||||
categories =
|
||||
mapAttrs
|
||||
# Convert each group from a list of key-value pairs back to an attrset
|
||||
(_: listToAttrs)
|
||||
(groupBy (cmd: cmd.value.category) (attrsToList allOptions));
|
||||
in
|
||||
concatStrings (attrValues (mapAttrs showCategory categories));
|
||||
in
|
||||
squash result;
|
||||
|
||||
appendName = filename: name: (if filename == "nix" then "nix3" else filename) + "-" + name;
|
||||
|
||||
processCommand = { command, details, filename, toplevel }:
|
||||
processCommand =
|
||||
{
|
||||
command,
|
||||
details,
|
||||
filename,
|
||||
toplevel,
|
||||
}:
|
||||
let
|
||||
cmd = {
|
||||
inherit command;
|
||||
name = filename + ".md";
|
||||
value = showCommand { inherit command details filename toplevel; };
|
||||
value = showCommand {
|
||||
inherit
|
||||
command
|
||||
details
|
||||
filename
|
||||
toplevel
|
||||
;
|
||||
};
|
||||
};
|
||||
subcommand = subCmd: processCommand {
|
||||
command = command + " " + subCmd;
|
||||
details = details.commands.${subCmd};
|
||||
filename = appendName filename subCmd;
|
||||
inherit toplevel;
|
||||
};
|
||||
in [ cmd ] ++ concatMap subcommand (attrNames details.commands or {});
|
||||
subcommand =
|
||||
subCmd:
|
||||
processCommand {
|
||||
command = command + " " + subCmd;
|
||||
details = details.commands.${subCmd};
|
||||
filename = appendName filename subCmd;
|
||||
inherit toplevel;
|
||||
};
|
||||
in
|
||||
[ cmd ] ++ concatMap subcommand (attrNames details.commands or { });
|
||||
|
||||
manpages = processCommand {
|
||||
command = "nix";
|
||||
|
|
@ -199,9 +225,11 @@ let
|
|||
toplevel = commandInfo.args;
|
||||
};
|
||||
|
||||
tableOfContents = let
|
||||
showEntry = page:
|
||||
" - [${page.command}](command-ref/new-cli/${page.name})";
|
||||
in concatStringsSep "\n" (map showEntry manpages) + "\n";
|
||||
tableOfContents =
|
||||
let
|
||||
showEntry = page: " - [${page.command}](command-ref/new-cli/${page.name})";
|
||||
in
|
||||
concatStringsSep "\n" (map showEntry manpages) + "\n";
|
||||
|
||||
in (listToAttrs manpages) // { "SUMMARY.md" = tableOfContents; }
|
||||
in
|
||||
(listToAttrs manpages) // { "SUMMARY.md" = tableOfContents; }
|
||||
|
|
|
|||
|
|
@ -1,67 +1,99 @@
|
|||
let
|
||||
inherit (builtins) attrValues concatStringsSep isAttrs isBool mapAttrs;
|
||||
inherit (import <nix/utils.nix>) concatStrings indent optionalString squash;
|
||||
inherit (builtins)
|
||||
attrValues
|
||||
concatStringsSep
|
||||
isAttrs
|
||||
isBool
|
||||
mapAttrs
|
||||
;
|
||||
inherit (import <nix/utils.nix>)
|
||||
concatStrings
|
||||
indent
|
||||
optionalString
|
||||
squash
|
||||
;
|
||||
in
|
||||
|
||||
# `inlineHTML` is a hack to accommodate inconsistent output from `lowdown`
|
||||
{ prefix, inlineHTML ? true }: settingsInfo:
|
||||
{
|
||||
prefix,
|
||||
inlineHTML ? true,
|
||||
}:
|
||||
settingsInfo:
|
||||
|
||||
let
|
||||
|
||||
showSetting = prefix: setting: { description, documentDefault, defaultValue, aliases, value, experimentalFeature }:
|
||||
showSetting =
|
||||
prefix: setting:
|
||||
{
|
||||
description,
|
||||
documentDefault,
|
||||
defaultValue,
|
||||
aliases,
|
||||
value,
|
||||
experimentalFeature,
|
||||
}:
|
||||
let
|
||||
result = squash ''
|
||||
- ${item}
|
||||
- ${item}
|
||||
|
||||
${indent " " body}
|
||||
'';
|
||||
item = if inlineHTML
|
||||
then ''<span id="${prefix}-${setting}">[`${setting}`](#${prefix}-${setting})</span>''
|
||||
else "`${setting}`";
|
||||
${indent " " body}
|
||||
'';
|
||||
item =
|
||||
if inlineHTML then
|
||||
''<span id="${prefix}-${setting}">[`${setting}`](#${prefix}-${setting})</span>''
|
||||
else
|
||||
"`${setting}`";
|
||||
# separate body to cleanly handle indentation
|
||||
body = ''
|
||||
${experimentalFeatureNote}
|
||||
${experimentalFeatureNote}
|
||||
|
||||
${description}
|
||||
${description}
|
||||
|
||||
**Default:** ${showDefault documentDefault defaultValue}
|
||||
**Default:** ${showDefault documentDefault defaultValue}
|
||||
|
||||
${showAliases aliases}
|
||||
'';
|
||||
${showAliases aliases}
|
||||
'';
|
||||
|
||||
experimentalFeatureNote = optionalString (experimentalFeature != null) ''
|
||||
> **Warning**
|
||||
>
|
||||
> This setting is part of an
|
||||
> [experimental feature](@docroot@/development/experimental-features.md).
|
||||
>
|
||||
> To change this setting, make sure the
|
||||
> [`${experimentalFeature}` experimental feature](@docroot@/development/experimental-features.md#xp-feature-${experimentalFeature})
|
||||
> is enabled.
|
||||
> For example, include the following in [`nix.conf`](@docroot@/command-ref/conf-file.md):
|
||||
>
|
||||
> ```
|
||||
> extra-experimental-features = ${experimentalFeature}
|
||||
> ${setting} = ...
|
||||
> ```
|
||||
'';
|
||||
> **Warning**
|
||||
>
|
||||
> This setting is part of an
|
||||
> [experimental feature](@docroot@/development/experimental-features.md).
|
||||
>
|
||||
> To change this setting, make sure the
|
||||
> [`${experimentalFeature}` experimental feature](@docroot@/development/experimental-features.md#xp-feature-${experimentalFeature})
|
||||
> is enabled.
|
||||
> For example, include the following in [`nix.conf`](@docroot@/command-ref/conf-file.md):
|
||||
>
|
||||
> ```
|
||||
> extra-experimental-features = ${experimentalFeature}
|
||||
> ${setting} = ...
|
||||
> ```
|
||||
'';
|
||||
|
||||
showDefault = documentDefault: defaultValue:
|
||||
showDefault =
|
||||
documentDefault: defaultValue:
|
||||
if documentDefault then
|
||||
# a StringMap value type is specified as a string, but
|
||||
# this shows the value type. The empty stringmap is `null` in
|
||||
# JSON, but that converts to `{ }` here.
|
||||
if defaultValue == "" || defaultValue == [] || isAttrs defaultValue
|
||||
then "*empty*"
|
||||
else if isBool defaultValue then
|
||||
if defaultValue then "`true`" else "`false`"
|
||||
else "`${toString defaultValue}`"
|
||||
else "*machine-specific*";
|
||||
if defaultValue == "" || defaultValue == [ ] || isAttrs defaultValue then
|
||||
"*empty*"
|
||||
else if isBool defaultValue then
|
||||
if defaultValue then "`true`" else "`false`"
|
||||
else
|
||||
"`${toString defaultValue}`"
|
||||
else
|
||||
"*machine-specific*";
|
||||
|
||||
showAliases = aliases:
|
||||
optionalString (aliases != [])
|
||||
"**Deprecated alias:** ${(concatStringsSep ", " (map (s: "`${s}`") aliases))}";
|
||||
showAliases =
|
||||
aliases:
|
||||
optionalString (aliases != [ ])
|
||||
"**Deprecated alias:** ${(concatStringsSep ", " (map (s: "`${s}`") aliases))}";
|
||||
|
||||
in result;
|
||||
in
|
||||
result;
|
||||
|
||||
in concatStrings (attrValues (mapAttrs (showSetting prefix) settingsInfo))
|
||||
in
|
||||
concatStrings (attrValues (mapAttrs (showSetting prefix) settingsInfo))
|
||||
|
|
|
|||
|
|
@ -1,6 +1,20 @@
|
|||
let
|
||||
inherit (builtins) attrNames listToAttrs concatStringsSep readFile replaceStrings;
|
||||
inherit (import <nix/utils.nix>) optionalString filterAttrs trim squash toLower unique indent;
|
||||
inherit (builtins)
|
||||
attrNames
|
||||
listToAttrs
|
||||
concatStringsSep
|
||||
readFile
|
||||
replaceStrings
|
||||
;
|
||||
inherit (import <nix/utils.nix>)
|
||||
optionalString
|
||||
filterAttrs
|
||||
trim
|
||||
squash
|
||||
toLower
|
||||
unique
|
||||
indent
|
||||
;
|
||||
showSettings = import <nix/generate-settings.nix>;
|
||||
in
|
||||
|
||||
|
|
@ -14,7 +28,13 @@ in
|
|||
|
||||
let
|
||||
|
||||
showStore = { name, slug }: { settings, doc, experimentalFeature }:
|
||||
showStore =
|
||||
{ name, slug }:
|
||||
{
|
||||
settings,
|
||||
doc,
|
||||
experimentalFeature,
|
||||
}:
|
||||
let
|
||||
result = squash ''
|
||||
# ${name}
|
||||
|
|
@ -25,7 +45,10 @@ let
|
|||
|
||||
## Settings
|
||||
|
||||
${showSettings { prefix = "store-${slug}"; inherit inlineHTML; } settings}
|
||||
${showSettings {
|
||||
prefix = "store-${slug}";
|
||||
inherit inlineHTML;
|
||||
} settings}
|
||||
'';
|
||||
|
||||
experimentalFeatureNote = optionalString (experimentalFeature != null) ''
|
||||
|
|
@ -43,15 +66,15 @@ let
|
|||
> extra-experimental-features = ${experimentalFeature}
|
||||
> ```
|
||||
'';
|
||||
in result;
|
||||
in
|
||||
result;
|
||||
|
||||
storesList = map
|
||||
(name: rec {
|
||||
inherit name;
|
||||
slug = replaceStrings [ " " ] [ "-" ] (toLower name);
|
||||
filename = "${slug}.md";
|
||||
page = showStore { inherit name slug; } storeInfo.${name};
|
||||
})
|
||||
(attrNames storeInfo);
|
||||
storesList = map (name: rec {
|
||||
inherit name;
|
||||
slug = replaceStrings [ " " ] [ "-" ] (toLower name);
|
||||
filename = "${slug}.md";
|
||||
page = showStore { inherit name slug; } storeInfo.${name};
|
||||
}) (attrNames storeInfo);
|
||||
|
||||
in storesList
|
||||
in
|
||||
storesList
|
||||
|
|
|
|||
|
|
@ -1,5 +1,11 @@
|
|||
let
|
||||
inherit (builtins) attrNames listToAttrs concatStringsSep readFile replaceStrings;
|
||||
inherit (builtins)
|
||||
attrNames
|
||||
listToAttrs
|
||||
concatStringsSep
|
||||
readFile
|
||||
replaceStrings
|
||||
;
|
||||
showSettings = import <nix/generate-settings.nix>;
|
||||
showStoreDocs = import <nix/generate-store-info.nix>;
|
||||
in
|
||||
|
|
@ -14,26 +20,28 @@ let
|
|||
|
||||
index =
|
||||
let
|
||||
showEntry = store:
|
||||
"- [${store.name}](./${store.filename})";
|
||||
showEntry = store: "- [${store.name}](./${store.filename})";
|
||||
in
|
||||
concatStringsSep "\n" (map showEntry storesList);
|
||||
|
||||
"index.md" = replaceStrings
|
||||
[ "@store-types@" ] [ index ]
|
||||
(readFile ./source/store/types/index.md.in);
|
||||
"index.md" =
|
||||
replaceStrings [ "@store-types@" ] [ index ]
|
||||
(readFile ./source/store/types/index.md.in);
|
||||
|
||||
tableOfContents =
|
||||
let
|
||||
showEntry = store:
|
||||
" - [${store.name}](store/types/${store.filename})";
|
||||
showEntry = store: " - [${store.name}](store/types/${store.filename})";
|
||||
in
|
||||
concatStringsSep "\n" (map showEntry storesList) + "\n";
|
||||
|
||||
"SUMMARY.md" = tableOfContents;
|
||||
|
||||
storePages = listToAttrs
|
||||
(map (s: { name = s.filename; value = s.page; }) storesList);
|
||||
storePages = listToAttrs (
|
||||
map (s: {
|
||||
name = s.filename;
|
||||
value = s.page;
|
||||
}) storesList
|
||||
);
|
||||
|
||||
in
|
||||
storePages // { inherit "index.md" "SUMMARY.md"; }
|
||||
|
|
|
|||
|
|
@ -2,8 +2,8 @@ with builtins;
|
|||
with import <nix/utils.nix>;
|
||||
|
||||
let
|
||||
showExperimentalFeature = name: doc:
|
||||
''
|
||||
- [`${name}`](@docroot@/development/experimental-features.md#xp-feature-${name})
|
||||
'';
|
||||
in xps: indent " " (concatStrings (attrValues (mapAttrs showExperimentalFeature xps)))
|
||||
showExperimentalFeature = name: doc: ''
|
||||
- [`${name}`](@docroot@/development/experimental-features.md#xp-feature-${name})
|
||||
'';
|
||||
in
|
||||
xps: indent " " (concatStrings (attrValues (mapAttrs showExperimentalFeature xps)))
|
||||
|
|
|
|||
|
|
@ -2,7 +2,8 @@ with builtins;
|
|||
with import <nix/utils.nix>;
|
||||
|
||||
let
|
||||
showExperimentalFeature = name: doc:
|
||||
showExperimentalFeature =
|
||||
name: doc:
|
||||
squash ''
|
||||
## [`${name}`]{#xp-feature-${name}}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,19 +1,20 @@
|
|||
{ lib
|
||||
, mkMesonDerivation
|
||||
{
|
||||
lib,
|
||||
mkMesonDerivation,
|
||||
|
||||
, meson
|
||||
, ninja
|
||||
, lowdown-unsandboxed
|
||||
, mdbook
|
||||
, mdbook-linkcheck
|
||||
, jq
|
||||
, python3
|
||||
, rsync
|
||||
, nix-cli
|
||||
meson,
|
||||
ninja,
|
||||
lowdown-unsandboxed,
|
||||
mdbook,
|
||||
mdbook-linkcheck,
|
||||
jq,
|
||||
python3,
|
||||
rsync,
|
||||
nix-cli,
|
||||
|
||||
# Configuration Options
|
||||
# Configuration Options
|
||||
|
||||
, version
|
||||
version,
|
||||
}:
|
||||
|
||||
let
|
||||
|
|
@ -25,18 +26,22 @@ mkMesonDerivation (finalAttrs: {
|
|||
inherit version;
|
||||
|
||||
workDir = ./.;
|
||||
fileset = fileset.difference
|
||||
(fileset.unions [
|
||||
../../.version
|
||||
# Too many different types of files to filter for now
|
||||
../../doc/manual
|
||||
./.
|
||||
])
|
||||
# Do a blacklist instead
|
||||
../../doc/manual/package.nix;
|
||||
fileset =
|
||||
fileset.difference
|
||||
(fileset.unions [
|
||||
../../.version
|
||||
# Too many different types of files to filter for now
|
||||
../../doc/manual
|
||||
./.
|
||||
])
|
||||
# Do a blacklist instead
|
||||
../../doc/manual/package.nix;
|
||||
|
||||
# TODO the man pages should probably be separate
|
||||
outputs = [ "out" "man" ];
|
||||
outputs = [
|
||||
"out"
|
||||
"man"
|
||||
];
|
||||
|
||||
# Hack for sake of the dev shell
|
||||
passthru.externalNativeBuildInputs = [
|
||||
|
|
@ -54,11 +59,10 @@ mkMesonDerivation (finalAttrs: {
|
|||
nix-cli
|
||||
];
|
||||
|
||||
preConfigure =
|
||||
''
|
||||
chmod u+w ./.version
|
||||
echo ${finalAttrs.version} > ./.version
|
||||
'';
|
||||
preConfigure = ''
|
||||
chmod u+w ./.version
|
||||
echo ${finalAttrs.version} > ./.version
|
||||
'';
|
||||
|
||||
postInstall = ''
|
||||
mkdir -p ''$out/nix-support
|
||||
|
|
|
|||
|
|
@ -1,22 +0,0 @@
|
|||
---
|
||||
synopsis: "Flake lock file generation now ignores local registries"
|
||||
prs: [12019]
|
||||
---
|
||||
|
||||
When resolving indirect flake references like `nixpkgs` in `flake.nix` files, Nix will no longer use the system and user flake registries. It will only use the global flake registry and overrides given on the command line via `--override-flake`.
|
||||
|
||||
This avoids accidents where users have local registry overrides that map `nixpkgs` to a `path:` flake in the local file system, which then end up in committed lock files pushed to other users.
|
||||
|
||||
In the future, we may remove the use of the registry during lock file generation altogether. It's better to explicitly specify the URL of a flake input. For example, instead of
|
||||
```nix
|
||||
{
|
||||
outputs = { self, nixpkgs }: { ... };
|
||||
}
|
||||
```
|
||||
write
|
||||
```nix
|
||||
{
|
||||
inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-24.11";
|
||||
outputs = { self, nixpkgs }: { ... };
|
||||
}
|
||||
```
|
||||
|
|
@ -1,18 +0,0 @@
|
|||
---
|
||||
synopsis: "`nix copy` supports `--profile` and `--out-link`"
|
||||
prs: [11657]
|
||||
---
|
||||
|
||||
The `nix copy` command now has flags `--profile` and `--out-link`, similar to `nix build`. `--profile` makes a profile point to the
|
||||
top-level store path, while `--out-link` create symlinks to the top-level store paths.
|
||||
|
||||
For example, when updating the local NixOS system profile from a NixOS system closure on a remote machine, instead of
|
||||
```
|
||||
# nix copy --from ssh://server $path
|
||||
# nix build --profile /nix/var/nix/profiles/system $path
|
||||
```
|
||||
you can now do
|
||||
```
|
||||
# nix copy --from ssh://server --profile /nix/var/nix/profiles/system $path
|
||||
```
|
||||
The advantage is that this avoids a time window where *path* is not a garbage collector root, and so could be deleted by a concurrent `nix store gc` process.
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
---
|
||||
synopsis: "`nix-instantiate --eval` now supports `--raw`"
|
||||
prs: [12119]
|
||||
---
|
||||
|
||||
The `nix-instantiate --eval` command now supports a `--raw` flag, when used
|
||||
the evaluation result must be a string, which is printed verbatim without
|
||||
quotation marks or escaping.
|
||||
|
|
@ -1,21 +0,0 @@
|
|||
---
|
||||
synopsis: "Improved `NIX_SSHOPTS` parsing for better SSH option handling"
|
||||
issues: [5181]
|
||||
prs: [12020]
|
||||
---
|
||||
|
||||
The parsing of the `NIX_SSHOPTS` environment variable has been improved to handle spaces and quotes correctly.
|
||||
Previously, incorrectly split SSH options could cause failures in CLIs like `nix-copy-closure`,
|
||||
especially when using complex ssh invocations such as `-o ProxyCommand="ssh -W %h:%p ..."`.
|
||||
|
||||
This change introduces a `shellSplitString` function to ensure
|
||||
that `NIX_SSHOPTS` is parsed in a manner consistent with shell
|
||||
behavior, addressing common parsing errors.
|
||||
|
||||
For example, the following now works as expected:
|
||||
|
||||
```bash
|
||||
export NIX_SSHOPTS='-o ProxyCommand="ssh -W %h:%p ..."'
|
||||
```
|
||||
|
||||
This update improves the reliability of SSH-related operations using `NIX_SSHOPTS` across Nix CLIs.
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
---
|
||||
synopsis: "Support for relative path inputs"
|
||||
prs: [10089]
|
||||
---
|
||||
|
||||
Flakes can now refer to other flakes in the same repository using relative paths, e.g.
|
||||
```nix
|
||||
inputs.foo.url = "path:./foo";
|
||||
```
|
||||
uses the flake in the `foo` subdirectory of the referring flake. For more information, see the documentation on [the `path` flake input type](@docroot@/command-ref/new-cli/nix3-flake.md#path-fetcher).
|
||||
|
||||
This feature required a change to the lock file format. Previous Nix versions will not be able to use lock files that have locks for relative path inputs in them.
|
||||
|
|
@ -132,6 +132,7 @@
|
|||
- [Contributing](development/contributing.md)
|
||||
- [Releases](release-notes/index.md)
|
||||
{{#include ./SUMMARY-rl-next.md}}
|
||||
- [Release 2.26 (2025-01-22)](release-notes/rl-2.26.md)
|
||||
- [Release 2.25 (2024-11-07)](release-notes/rl-2.25.md)
|
||||
- [Release 2.24 (2024-07-31)](release-notes/rl-2.24.md)
|
||||
- [Release 2.23 (2024-06-03)](release-notes/rl-2.23.md)
|
||||
|
|
|
|||
|
|
@ -79,7 +79,7 @@ This shell also adds `./outputs/bin/nix` to your `$PATH` so you can run `nix` im
|
|||
To get a shell with one of the other [supported compilation environments](#compilation-environments):
|
||||
|
||||
```console
|
||||
$ nix develop .#native-clangStdenvPackages
|
||||
$ nix develop .#native-clangStdenv
|
||||
```
|
||||
|
||||
> **Note**
|
||||
|
|
@ -167,11 +167,13 @@ It is useful to perform multiple cross and native builds on the same source tree
|
|||
for example to ensure that better support for one platform doesn't break the build for another.
|
||||
Meson thankfully makes this very easy by confining all build products to the build directory --- one simple shares the source directory between multiple build directories, each of which contains the build for Nix to a different platform.
|
||||
|
||||
Nixpkgs's `configurePhase` always chooses `build` in the current directory as the name and location of the build.
|
||||
This makes having multiple build directories slightly more inconvenient.
|
||||
The good news is that Meson/Ninja seem to cope well with relocating the build directory after it is created.
|
||||
Here's how to do that:
|
||||
|
||||
Here's how to do that
|
||||
1. Instruct Nixpkgs's infra where we want Meson to put its build directory
|
||||
|
||||
```bash
|
||||
mesonBuildDir=build-my-variant-name
|
||||
```
|
||||
|
||||
1. Configure as usual
|
||||
|
||||
|
|
@ -179,24 +181,12 @@ Here's how to do that
|
|||
configurePhase
|
||||
```
|
||||
|
||||
2. Rename the build directory
|
||||
|
||||
```bash
|
||||
cd .. # since `configurePhase` cd'd inside
|
||||
mv build build-linux # or whatever name we want
|
||||
cd build-linux
|
||||
```
|
||||
|
||||
3. Build as usual
|
||||
|
||||
```bash
|
||||
buildPhase
|
||||
```
|
||||
|
||||
> **N.B.**
|
||||
> [`nixpkgs#335818`](https://github.com/NixOS/nixpkgs/issues/335818) tracks giving `mesonConfigurePhase` proper support for custom build directories.
|
||||
> When it is fixed, we can simplify these instructions and then remove this notice.
|
||||
|
||||
## System type
|
||||
|
||||
Nix uses a string with the following format to identify the *system type* or *platform* it runs on:
|
||||
|
|
@ -261,7 +251,8 @@ See [supported compilation environments](#compilation-environments) and instruct
|
|||
To use the LSP with your editor, you will want a `compile_commands.json` file telling `clangd` how we are compiling the code.
|
||||
Meson's configure always produces this inside the build directory.
|
||||
|
||||
Configure your editor to use the `clangd` from the `.#native-clangStdenvPackages` shell. You can do that either by running it inside the development shell, or by using [nix-direnv](https://github.com/nix-community/nix-direnv) and [the appropriate editor plugin](https://github.com/direnv/direnv/wiki#editor-integration).
|
||||
Configure your editor to use the `clangd` from the `.#native-clangStdenv` shell.
|
||||
You can do that either by running it inside the development shell, or by using [nix-direnv](https://github.com/nix-community/nix-direnv) and [the appropriate editor plugin](https://github.com/direnv/direnv/wiki#editor-integration).
|
||||
|
||||
> **Note**
|
||||
>
|
||||
|
|
@ -277,6 +268,8 @@ You may run the formatters as a one-off using:
|
|||
./maintainers/format.sh
|
||||
```
|
||||
|
||||
### Pre-commit hooks
|
||||
|
||||
If you'd like to run the formatters before every commit, install the hooks:
|
||||
|
||||
```
|
||||
|
|
@ -291,3 +284,30 @@ If it fails, run `git add --patch` to approve the suggestions _and commit again_
|
|||
To refresh pre-commit hook's config file, do the following:
|
||||
1. Exit the development shell and start it again by running `nix develop`.
|
||||
2. If you also use the pre-commit hook, also run `pre-commit-hooks-install` again.
|
||||
|
||||
### VSCode
|
||||
|
||||
Insert the following json into your `.vscode/settings.json` file to configure `nixfmt`.
|
||||
This will be picked up by the _Format Document_ command, `"editor.formatOnSave"`, etc.
|
||||
|
||||
```json
|
||||
{
|
||||
"nix.formatterPath": "nixfmt",
|
||||
"nix.serverSettings": {
|
||||
"nixd": {
|
||||
"formatting": {
|
||||
"command": [
|
||||
"nixfmt"
|
||||
],
|
||||
},
|
||||
},
|
||||
"nil": {
|
||||
"formatting": {
|
||||
"command": [
|
||||
"nixfmt"
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@
|
|||
|
||||
This section shows how to build and debug Nix with debug symbols enabled.
|
||||
|
||||
Additionally, see [Testing Nix](./testing.md) for further instructions on how to debug Nix in the context of a unit test or functional test.
|
||||
|
||||
## Building Nix with Debug Symbols
|
||||
|
||||
In the development shell, set the `mesonBuildType` environment variable to `debug` before configuring the build:
|
||||
|
|
@ -13,6 +15,15 @@ In the development shell, set the `mesonBuildType` environment variable to `debu
|
|||
Then, proceed to build Nix as described in [Building Nix](./building.md).
|
||||
This will build Nix with debug symbols, which are essential for effective debugging.
|
||||
|
||||
It is also possible to build without debugging for faster build:
|
||||
|
||||
```console
|
||||
[nix-shell]$ NIX_HARDENING_ENABLE=$(printLines $NIX_HARDENING_ENABLE | grep -v fortify)
|
||||
[nix-shell]$ export mesonBuildType=debug
|
||||
```
|
||||
|
||||
(The first line is needed because `fortify` hardening requires at least some optimization.)
|
||||
|
||||
## Debugging the Nix Binary
|
||||
|
||||
Obtain your preferred debugger within the development shell:
|
||||
|
|
|
|||
|
|
@ -87,7 +87,11 @@ A environment variables that Google Test accepts are also worth knowing:
|
|||
|
||||
This is used to avoid logging passing tests.
|
||||
|
||||
Putting the two together, one might run
|
||||
3. [`GTEST_BREAK_ON_FAILURE`](https://google.github.io/googletest/advanced.html#turning-assertion-failures-into-break-points)
|
||||
|
||||
This is used to create a debugger breakpoint when an assertion failure occurs.
|
||||
|
||||
Putting the first two together, one might run
|
||||
|
||||
```bash
|
||||
GTEST_BRIEF=1 GTEST_FILTER='ErrorTraceTest.*' meson test nix-expr-tests -v
|
||||
|
|
@ -95,6 +99,22 @@ GTEST_BRIEF=1 GTEST_FILTER='ErrorTraceTest.*' meson test nix-expr-tests -v
|
|||
|
||||
for short but comprensive output.
|
||||
|
||||
### Debugging tests
|
||||
|
||||
For debugging, it is useful to combine the third option above with Meson's [`--gdb`](https://mesonbuild.com/Unit-tests.html#other-test-options) flag:
|
||||
|
||||
```bash
|
||||
GTEST_BRIEF=1 GTEST_FILTER='Group.my-failing-test' meson test nix-expr-tests --gdb
|
||||
```
|
||||
|
||||
This will:
|
||||
|
||||
1. Run the unit test with GDB
|
||||
|
||||
2. Run just `Group.my-failing-test`
|
||||
|
||||
3. Stop the program when the test fails, allowing the user to then issue arbitrary commands to GDB.
|
||||
|
||||
### Characterisation testing { #characaterisation-testing-unit }
|
||||
|
||||
See [functional characterisation testing](#characterisation-testing-functional) for a broader discussion of characterisation testing.
|
||||
|
|
@ -213,10 +233,10 @@ edit it like so:
|
|||
bar
|
||||
```
|
||||
|
||||
Then, running the test with `./mk/debug-test.sh` will drop you into GDB once the script reaches that point:
|
||||
Then, running the test with [`--interactive`](https://mesonbuild.com/Unit-tests.html#other-test-options) will prevent Meson from hijacking the terminal so you can drop you into GDB once the script reaches that point:
|
||||
|
||||
```shell-session
|
||||
$ ./mk/debug-test.sh tests/functional/${testName}.sh
|
||||
$ meson test ${testName} --interactive
|
||||
...
|
||||
+ gdb blash blub
|
||||
GNU gdb (GDB) 12.1
|
||||
|
|
|
|||
128
doc/manual/source/release-notes/rl-2.26.md
Normal file
128
doc/manual/source/release-notes/rl-2.26.md
Normal file
|
|
@ -0,0 +1,128 @@
|
|||
# Release 2.26.0 (2025-01-22)
|
||||
|
||||
- Support for relative path inputs [#10089](https://github.com/NixOS/nix/pull/10089)
|
||||
|
||||
Flakes can now refer to other flakes in the same repository using relative paths, e.g.
|
||||
```nix
|
||||
inputs.foo.url = "path:./foo";
|
||||
```
|
||||
uses the flake in the `foo` subdirectory of the referring flake. For more information, see the documentation on [the `path` flake input type](@docroot@/command-ref/new-cli/nix3-flake.md#path-fetcher).
|
||||
|
||||
This feature required a change to the lock file format. Previous Nix versions will not be able to use lock files that have locks for relative path inputs in them.
|
||||
|
||||
- Flake lock file generation now ignores local registries [#12019](https://github.com/NixOS/nix/pull/12019)
|
||||
|
||||
When resolving indirect flake references like `nixpkgs` in `flake.nix` files, Nix will no longer use the system and user flake registries. It will only use the global flake registry and overrides given on the command line via `--override-flake`.
|
||||
|
||||
This avoids accidents where users have local registry overrides that map `nixpkgs` to a `path:` flake in the local file system, which then end up in committed lock files pushed to other users.
|
||||
|
||||
In the future, we may remove the use of the registry during lock file generation altogether. It's better to explicitly specify the URL of a flake input. For example, instead of
|
||||
```nix
|
||||
{
|
||||
outputs = { self, nixpkgs }: { ... };
|
||||
}
|
||||
```
|
||||
write
|
||||
```nix
|
||||
{
|
||||
inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-24.11";
|
||||
outputs = { self, nixpkgs }: { ... };
|
||||
}
|
||||
```
|
||||
|
||||
- `nix copy` supports `--profile` and `--out-link` [#11657](https://github.com/NixOS/nix/pull/11657)
|
||||
|
||||
The `nix copy` command now has flags `--profile` and `--out-link`, similar to `nix build`. `--profile` makes a profile point to the
|
||||
top-level store path, while `--out-link` create symlinks to the top-level store paths.
|
||||
|
||||
For example, when updating the local NixOS system profile from a NixOS system closure on a remote machine, instead of
|
||||
```
|
||||
# nix copy --from ssh://server $path
|
||||
# nix build --profile /nix/var/nix/profiles/system $path
|
||||
```
|
||||
you can now do
|
||||
```
|
||||
# nix copy --from ssh://server --profile /nix/var/nix/profiles/system $path
|
||||
```
|
||||
The advantage is that this avoids a time window where *path* is not a garbage collector root, and so could be deleted by a concurrent `nix store gc` process.
|
||||
|
||||
- `nix-instantiate --eval` now supports `--raw` [#12119](https://github.com/NixOS/nix/pull/12119)
|
||||
|
||||
The `nix-instantiate --eval` command now supports a `--raw` flag, when used
|
||||
the evaluation result must be a string, which is printed verbatim without
|
||||
quotation marks or escaping.
|
||||
|
||||
- Improved `NIX_SSHOPTS` parsing for better SSH option handling [#5181](https://github.com/NixOS/nix/issues/5181) [#12020](https://github.com/NixOS/nix/pull/12020)
|
||||
|
||||
The parsing of the `NIX_SSHOPTS` environment variable has been improved to handle spaces and quotes correctly.
|
||||
Previously, incorrectly split SSH options could cause failures in commands like `nix-copy-closure`,
|
||||
especially when using complex SSH invocations such as `-o ProxyCommand="ssh -W %h:%p ..."`.
|
||||
|
||||
This change introduces a `shellSplitString` function to ensure
|
||||
that `NIX_SSHOPTS` is parsed in a manner consistent with shell
|
||||
behavior, addressing common parsing errors.
|
||||
|
||||
For example, the following now works as expected:
|
||||
|
||||
```bash
|
||||
export NIX_SSHOPTS='-o ProxyCommand="ssh -W %h:%p ..."'
|
||||
```
|
||||
|
||||
This update improves the reliability of SSH-related operations using `NIX_SSHOPTS` across Nix CLIs.
|
||||
|
||||
- Nix is now built using Meson
|
||||
|
||||
As proposed in [RFC 132](https://github.com/NixOS/rfcs/pull/132), Nix's build system now uses Meson/Ninja. The old Make-based build system has been removed.
|
||||
|
||||
- Evaluation caching now works for dirty Git workdirs [#11992](https://github.com/NixOS/nix/pull/11992)
|
||||
|
||||
# Contributors
|
||||
|
||||
This release was made possible by the following 45 contributors:
|
||||
|
||||
- Anatoli Babenia [**(@abitrolly)**](https://github.com/abitrolly)
|
||||
- Domagoj Mišković [**(@allrealmsoflife)**](https://github.com/allrealmsoflife)
|
||||
- Yaroslav Bolyukin [**(@CertainLach)**](https://github.com/CertainLach)
|
||||
- bryango [**(@bryango)**](https://github.com/bryango)
|
||||
- tomberek [**(@tomberek)**](https://github.com/tomberek)
|
||||
- Matej Urbas [**(@mupdt)**](https://github.com/mupdt)
|
||||
- elikoga [**(@elikoga)**](https://github.com/elikoga)
|
||||
- wh0 [**(@wh0)**](https://github.com/wh0)
|
||||
- Félix [**(@picnoir)**](https://github.com/picnoir)
|
||||
- Valentin Gagarin [**(@fricklerhandwerk)**](https://github.com/fricklerhandwerk)
|
||||
- Gavin John [**(@Pandapip1)**](https://github.com/Pandapip1)
|
||||
- Travis A. Everett [**(@abathur)**](https://github.com/abathur)
|
||||
- Vladimir Panteleev [**(@CyberShadow)**](https://github.com/CyberShadow)
|
||||
- Ilja [**(@suruaku)**](https://github.com/suruaku)
|
||||
- Jason Yundt [**(@Jayman2000)**](https://github.com/Jayman2000)
|
||||
- Mike Kusold [**(@kusold)**](https://github.com/kusold)
|
||||
- Andy Hamon [**(@andrewhamon)**](https://github.com/andrewhamon)
|
||||
- Brian McKenna [**(@puffnfresh)**](https://github.com/puffnfresh)
|
||||
- Greg Curtis [**(@gcurtis)**](https://github.com/gcurtis)
|
||||
- Andrew Poelstra [**(@apoelstra)**](https://github.com/apoelstra)
|
||||
- Linus Heckemann [**(@lheckemann)**](https://github.com/lheckemann)
|
||||
- Tristan Ross [**(@RossComputerGuy)**](https://github.com/RossComputerGuy)
|
||||
- Dominique Martinet [**(@martinetd)**](https://github.com/martinetd)
|
||||
- h0nIg [**(@h0nIg)**](https://github.com/h0nIg)
|
||||
- Eelco Dolstra [**(@edolstra)**](https://github.com/edolstra)
|
||||
- Shahar "Dawn" Or [**(@mightyiam)**](https://github.com/mightyiam)
|
||||
- NAHO [**(@trueNAHO)**](https://github.com/trueNAHO)
|
||||
- Ryan Hendrickson [**(@rhendric)**](https://github.com/rhendric)
|
||||
- the-sun-will-rise-tomorrow [**(@the-sun-will-rise-tomorrow)**](https://github.com/the-sun-will-rise-tomorrow)
|
||||
- Connor Baker [**(@ConnorBaker)**](https://github.com/ConnorBaker)
|
||||
- Cole Helbling [**(@cole-h)**](https://github.com/cole-h)
|
||||
- Jack Wilsdon [**(@jackwilsdon)**](https://github.com/jackwilsdon)
|
||||
- rekcäH nitraM [**(@dwt)**](https://github.com/dwt)
|
||||
- Martin Fischer [**(@not-my-profile)**](https://github.com/not-my-profile)
|
||||
- John Ericson [**(@Ericson2314)**](https://github.com/Ericson2314)
|
||||
- Graham Christensen [**(@grahamc)**](https://github.com/grahamc)
|
||||
- Sergei Zimmerman [**(@xokdvium)**](https://github.com/xokdvium)
|
||||
- Siddarth Kumar [**(@siddarthkay)**](https://github.com/siddarthkay)
|
||||
- Sergei Trofimovich [**(@trofi)**](https://github.com/trofi)
|
||||
- Robert Hensing [**(@roberth)**](https://github.com/roberth)
|
||||
- Mutsuha Asada [**(@momeemt)**](https://github.com/momeemt)
|
||||
- Parker Jones [**(@knotapun)**](https://github.com/knotapun)
|
||||
- Jörg Thalheim [**(@Mic92)**](https://github.com/Mic92)
|
||||
- dbdr [**(@dbdr)**](https://github.com/dbdr)
|
||||
- myclevorname [**(@myclevorname)**](https://github.com/myclevorname)
|
||||
- Philipp Otterbein
|
||||
|
|
@ -11,10 +11,15 @@ rec {
|
|||
|
||||
concatStrings = concatStringsSep "";
|
||||
|
||||
attrsToList = a:
|
||||
map (name: { inherit name; value = a.${name}; }) (builtins.attrNames a);
|
||||
attrsToList =
|
||||
a:
|
||||
map (name: {
|
||||
inherit name;
|
||||
value = a.${name};
|
||||
}) (builtins.attrNames a);
|
||||
|
||||
replaceStringsRec = from: to: string:
|
||||
replaceStringsRec =
|
||||
from: to: string:
|
||||
# recursively replace occurrences of `from` with `to` within `string`
|
||||
# example:
|
||||
# replaceStringRec "--" "-" "hello-----world"
|
||||
|
|
@ -22,16 +27,18 @@ rec {
|
|||
let
|
||||
replaced = replaceStrings [ from ] [ to ] string;
|
||||
in
|
||||
if replaced == string then string else replaceStringsRec from to replaced;
|
||||
if replaced == string then string else replaceStringsRec from to replaced;
|
||||
|
||||
toLower = replaceStrings upperChars lowerChars;
|
||||
|
||||
squash = replaceStringsRec "\n\n\n" "\n\n";
|
||||
|
||||
trim = string:
|
||||
trim =
|
||||
string:
|
||||
# trim trailing spaces and squash non-leading spaces
|
||||
let
|
||||
trimLine = line:
|
||||
trimLine =
|
||||
line:
|
||||
let
|
||||
# separate leading spaces from the rest
|
||||
parts = split "(^ *)" line;
|
||||
|
|
@ -39,19 +46,30 @@ rec {
|
|||
rest = elemAt parts 2;
|
||||
# drop trailing spaces
|
||||
body = head (split " *$" rest);
|
||||
in spaces + replaceStringsRec " " " " body;
|
||||
in concatStringsSep "\n" (map trimLine (splitLines string));
|
||||
in
|
||||
spaces + replaceStringsRec " " " " body;
|
||||
in
|
||||
concatStringsSep "\n" (map trimLine (splitLines string));
|
||||
|
||||
# FIXME: O(n^2)
|
||||
unique = foldl' (acc: e: if elem e acc then acc else acc ++ [ e ]) [];
|
||||
unique = foldl' (acc: e: if elem e acc then acc else acc ++ [ e ]) [ ];
|
||||
|
||||
nameValuePair = name: value: { inherit name value; };
|
||||
|
||||
filterAttrs = pred: set:
|
||||
listToAttrs (concatMap (name: let v = set.${name}; in if pred name v then [(nameValuePair name v)] else []) (attrNames set));
|
||||
filterAttrs =
|
||||
pred: set:
|
||||
listToAttrs (
|
||||
concatMap (
|
||||
name:
|
||||
let
|
||||
v = set.${name};
|
||||
in
|
||||
if pred name v then [ (nameValuePair name v) ] else [ ]
|
||||
) (attrNames set)
|
||||
);
|
||||
|
||||
optionalString = cond: string: if cond then string else "";
|
||||
|
||||
indent = prefix: s:
|
||||
concatStringsSep "\n" (map (x: if x == "" then x else "${prefix}${x}") (splitLines s));
|
||||
indent =
|
||||
prefix: s: concatStringsSep "\n" (map (x: if x == "" then x else "${prefix}${x}") (splitLines s));
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue