Merge staging-next into staging

This commit is contained in:
github-actions[bot]
2024-11-30 06:05:05 +00:00
committed by GitHub
58 changed files with 4454 additions and 5214 deletions
+1 -1
View File
@@ -4,7 +4,7 @@ on:
pull_request_target:
paths:
- 'shell.nix'
- './ci/**'
- 'ci/**'
permissions: {}
-7
View File
@@ -13743,7 +13743,6 @@
github = "matthewpi";
githubId = 26559841;
name = "Matthew Penner";
keys = [ { fingerprint = "5118 F1CC B7B0 6C17 4DD1 5267 3131 1906 AD4C F6D6"; } ];
};
matthiasbenaets = {
email = "matthias.benaets@gmail.com";
@@ -21203,12 +21202,6 @@
githubId = 1694705;
name = "Sam Stites";
};
stnley = {
email = "michael@stnley.io";
github = "stnley";
githubId = 64174376;
name = "Michael Stanley";
};
strager = {
email = "strager.nds@gmail.com";
github = "strager";
@@ -25,6 +25,8 @@
- [Kimai](https://www.kimai.org/), a web-based multi-user time-tracking application. Available as [services.kimai](option.html#opt-services.kimai).
- [Omnom](https://github.com/asciimoo/omnom), a webpage bookmarking and snapshotting service. Available as [services.omnom](options.html#opt-services.omnom.enable).
- [Amazon CloudWatch Agent](https://github.com/aws/amazon-cloudwatch-agent), the official telemetry collector for AWS CloudWatch and AWS X-Ray. Available as [services.amazon-cloudwatch-agent](#opt-services.amazon-cloudwatch-agent.enable).
- [agorakit](https://github.com/agorakit/agorakit), an organization tool for citizens' collectives. Available with [services.agorakit](#opt-services.agorakit.enable).
@@ -60,6 +62,8 @@
[official website](https://www.nerdfonts.com/font-downloads) as the titles in preview images, with the "Nerd Font"
suffix and any whitespaces trimmed.
- `gkraken` software and `hardware.gkraken.enable` option have been removed, use `coolercontrol` via `programs.coolercontrol.enable` option instead.
- the notmuch vim plugin now lives in a separate output of the `notmuch`
package. Installing `notmuch` will not bring the notmuch vim package anymore,
add `vimPlugins.notmuch-vim` to your (Neo)vim configuration if you want the
-15
View File
@@ -1,15 +0,0 @@
{ config, lib, pkgs, ... }:
let
cfg = config.hardware.gkraken;
in
{
options.hardware.gkraken = {
enable = lib.mkEnableOption "gkraken's udev rules for NZXT AIO liquid coolers";
};
config = lib.mkIf cfg.enable {
services.udev.packages = with pkgs; [
gkraken
];
};
}
-1
View File
@@ -65,7 +65,6 @@
./hardware/digitalbitbox.nix
./hardware/flipperzero.nix
./hardware/flirc.nix
./hardware/gkraken.nix
./hardware/glasgow.nix
./hardware/gpgsmartcards.nix
./hardware/graphics.nix
+4
View File
@@ -33,6 +33,10 @@ in
systemd-logind API). Instead of using the module you can now
simply add the brightnessctl package to environment.systemPackages.
'')
(mkRemovedOptionModule [ "hardware" "gkraken" "enable" ] ''
gkraken was deprecated by coolercontrol and thus removed from nixpkgs.
Consider using programs.coolercontrol instead.
'')
(mkRemovedOptionModule [ "hardware" "u2f" ] ''
The U2F modules module was removed, as all it did was adding the
udev rules from libu2f-host to the system. Udev gained native support
+8
View File
@@ -672,10 +672,12 @@ let
"IPv6AcceptRA"
"IPv6DuplicateAddressDetection"
"IPv6HopLimit"
"IPv6RetransmissionTimeSec"
"IPv4ReversePathFilter"
"IPv4AcceptLocal"
"IPv4RouteLocalnet"
"IPv4ProxyARP"
"IPv4ProxyARPPrivateVLAN"
"IPv6ProxyNDP"
"IPv6ProxyNDPAddress"
"IPv6SendRA"
@@ -726,10 +728,12 @@ let
(assertMinimum "IPv6DuplicateAddressDetection" 0)
(assertInt "IPv6HopLimit")
(assertMinimum "IPv6HopLimit" 0)
(assertInt "IPv6RetransmissionTimeSec")
(assertValueOneOf "IPv4ReversePathFilter" ["no" "strict" "loose"])
(assertValueOneOf "IPv4AcceptLocal" boolValues)
(assertValueOneOf "IPv4RouteLocalnet" boolValues)
(assertValueOneOf "IPv4ProxyARP" boolValues)
(assertValueOneof "IPv4ProxyARPPrivateVLAN" boolValues)
(assertValueOneOf "IPv6ProxyNDP" boolValues)
(assertValueOneOf "IPv6SendRA" boolValues)
(assertValueOneOf "DHCPPrefixDelegation" boolValues)
@@ -776,6 +780,7 @@ let
"Priority"
"IncomingInterface"
"OutgoingInterface"
"L3MasterDevice"
"SourcePort"
"DestinationPort"
"IPProtocol"
@@ -790,6 +795,7 @@ let
(assertRange "TypeOfService" 0 255)
(assertRangeWithOptionalMask "FirewallMark" 1 4294967295)
(assertInt "Priority")
(assertValueOneOf "L3MasterDevice" boolValues)
(assertPortOrPortRange "SourcePort")
(assertPortOrPortRange "DestinationPort")
(assertValueOneOf "InvertRule" boolValues)
@@ -1033,6 +1039,7 @@ let
"BootServerName"
"BootFilename"
"IPv6OnlyPreferredSec"
"PersistLeases"
])
(assertInt "PoolOffset")
(assertMinimum "PoolOffset" 0)
@@ -1047,6 +1054,7 @@ let
(assertValueOneOf "EmitRouter" boolValues)
(assertValueOneOf "EmitTimezone" boolValues)
(assertValueOneOf "BindToInterface" boolValues)
(assertValueOneOf "PersistLeases" boolValues)
];
sectionIPv6SendRA = checkUnitConfig "IPv6SendRA" [
@@ -1,11 +1,11 @@
{
"packageVersion": "132.0.2-1",
"packageVersion": "133.0-1",
"source": {
"rev": "132.0.2-1",
"sha256": "7DB0QSQHNRw991yRR5+/Oo4fpXCR/Zklxb7ILRIH0WM="
"rev": "133.0-1",
"sha256": "1xf7gx3xm3c7dhch9gwpb0xp11lcyim1nrbm8sjljxdcs7iq9jy4"
},
"firefox": {
"version": "132.0.2",
"sha512": "nqldn7GpQaxaW1DaZ+Ik88z4xAHybLYbt0rX9OHocG1GnEtjJXFPLLnN9QwycQN31ryhjdZbVdssOe8rJ6V/rg=="
"version": "133.0",
"sha512": "b16f9898bee4121914caef48d4f7f44bf9d69aee168586b02bf1b4f4197844fd10179e1b63b273f52929fb348030df36328f24993cd666969da4ddc82562a90c"
}
}
+30 -63
View File
@@ -919,10 +919,19 @@ rec {
, includeStorePaths ? true
, includeNixDB ? false
, passthru ? {}
,
, # Pipeline used to produce docker layers. If not set, popularity contest
# algorithm is used. If set, maxLayers is ignored as the author of the
# pipeline can use one of the available functions (like "limit_layers")
# to control the amount of layers.
# See: pkgs/build-support/flatten-references-graph/src/flatten_references_graph/pipe.py
# for available functions, and it's test for how to use them.
# WARNING!! this interface is highly experimental and subject to change.
layeringPipeline ? null
, # Enables debug logging for the layering pipeline.
debug ? false
}:
assert
(lib.assertMsg (maxLayers > 1)
(lib.assertMsg (layeringPipeline == null -> maxLayers > 1)
"the maxLayers argument of dockerTools.buildLayeredImage function must be greather than 1 (current value: ${toString maxLayers})");
assert
(lib.assertMsg (enableFakechroot -> !stdenv.hostPlatform.isDarwin) ''
@@ -999,18 +1008,23 @@ rec {
'';
};
closureRoots = lib.optionals includeStorePaths /* normally true */ (
[ baseJson customisationLayer ]
);
overallClosure = writeText "closure" (lib.concatStringsSep " " closureRoots);
# These derivations are only created as implementation details of docker-tools,
# so they'll be excluded from the created images.
unnecessaryDrvs = [ baseJson overallClosure customisationLayer ];
layersJsonFile = buildPackages.dockerMakeLayers {
inherit debug;
closureRoots = optionals includeStorePaths [ baseJson customisationLayer ];
excludePaths = [ baseJson customisationLayer ];
pipeline =
if layeringPipeline != null
then layeringPipeline
else import
./popularity-contest-layering-pipeline.nix
{ inherit lib jq runCommand; }
{ inherit fromImage maxLayers; }
;
};
conf = runCommand "${baseName}-conf.json"
{
inherit fromImage maxLayers created mtime uid gid uname gname;
inherit fromImage created mtime uid gid uname gname layersJsonFile;
imageName = lib.toLower name;
preferLocalBuild = true;
passthru.imageTag =
@@ -1018,7 +1032,6 @@ rec {
then tag
else
lib.head (lib.strings.splitString "-" (baseNameOf (builtins.unsafeDiscardStringContext conf.outPath)));
paths = buildPackages.referencesByPopularity overallClosure;
nativeBuildInputs = [ jq ];
} ''
${if (tag == null) then ''
@@ -1038,54 +1051,7 @@ rec {
mtime="$(date -Iseconds -d "$mtime")"
fi
paths() {
cat $paths ${lib.concatMapStringsSep " "
(path: "| (grep -v ${path} || true)")
unnecessaryDrvs}
}
# Compute the number of layers that are already used by a potential
# 'fromImage' as well as the customization layer. Ensure that there is
# still at least one layer available to store the image contents.
usedLayers=0
# subtract number of base image layers
if [[ -n "$fromImage" ]]; then
(( usedLayers += $(tar -xOf "$fromImage" manifest.json | jq '.[0].Layers | length') ))
fi
# one layer will be taken up by the customisation layer
(( usedLayers += 1 ))
if ! (( $usedLayers < $maxLayers )); then
echo >&2 "Error: usedLayers $usedLayers layers to store 'fromImage' and" \
"'extraCommands', but only maxLayers=$maxLayers were" \
"allowed. At least 1 layer is required to store contents."
exit 1
fi
availableLayers=$(( maxLayers - usedLayers ))
# Create $maxLayers worth of Docker Layers, one layer per store path
# unless there are more paths than $maxLayers. In that case, create
# $maxLayers-1 for the most popular layers, and smush the remainaing
# store paths in to one final layer.
#
# The following code is fiddly w.r.t. ensuring every layer is
# created, and that no paths are missed. If you change the
# following lines, double-check that your code behaves properly
# when the number of layers equals:
# maxLayers-1, maxLayers, and maxLayers+1, 0
paths |
jq -sR '
rtrimstr("\n") | split("\n")
| (.[:$maxLayers-1] | map([.])) + [ .[$maxLayers-1:] ]
| map(select(length > 0))
' \
--argjson maxLayers "$availableLayers" > store_layers.json
# The index on $store_layers is necessary because the --slurpfile
# automatically reads the file as an array.
cat ${baseJson} | jq '
jq '
. + {
"store_dir": $store_dir,
"from_image": $from_image,
@@ -1101,7 +1067,7 @@ rec {
}
' --arg store_dir "${storeDir}" \
--argjson from_image ${if fromImage == null then "null" else "'\"${fromImage}\"'"} \
--slurpfile store_layers store_layers.json \
--slurpfile store_layers "$layersJsonFile" \
--arg customisation_layer ${customisationLayer} \
--arg repo_tag "$imageName:$imageTag" \
--arg created "$created" \
@@ -1109,8 +1075,9 @@ rec {
--arg uid "$uid" \
--arg gid "$gid" \
--arg uname "$uname" \
--arg gname "$gname" |
tee $out
--arg gname "$gname" \
${baseJson} \
| tee $out
'';
result = runCommand "stream-${baseName}"
+50
View File
@@ -0,0 +1,50 @@
{
coreutils,
flattenReferencesGraph,
lib,
jq,
runCommand,
}:
{
closureRoots,
excludePaths ? [ ],
# This could be a path to (or a derivation producing a path to)
# a json file containing the pipeline
pipeline ? [ ],
debug ? false,
}:
if closureRoots == [ ] then
builtins.toFile "docker-layers-empty" "[]"
else
runCommand "docker-layers"
{
__structuredAttrs = true;
# graph, exclude_paths and pipeline are expected by the
# flatten_references_graph executable.
exportReferencesGraph.graph = closureRoots;
exclude_paths = excludePaths;
inherit pipeline;
nativeBuildInputs = [
coreutils
flattenReferencesGraph
jq
];
}
''
. .attrs.sh
flatten_references_graph_arg=.attrs.json
echo "pipeline: $pipeline"
if jq -e '.pipeline | type == "string"' .attrs.json; then
jq '. + { "pipeline": $pipeline[0] }' \
--slurpfile pipeline "$pipeline" \
.attrs.json > flatten_references_graph_arg.json
flatten_references_graph_arg=flatten_references_graph_arg.json
fi
${lib.optionalString debug "export DEBUG=True"}
flatten_references_graph "$flatten_references_graph_arg" > ''${outputs[out]}
''
@@ -0,0 +1,34 @@
{
lib,
runCommand,
jq,
}:
{
maxLayers,
fromImage ? null,
}:
runCommand "popularity-contest-layering-pipeline.json" { inherit maxLayers; } ''
# Compute the number of layers that are already used by a potential
# 'fromImage' as well as the customization layer. Ensure that there is
# still at least one layer available to store the image contents.
# one layer will be taken up by the customisation layer
usedLayers=1
${lib.optionalString (fromImage != null) ''
# subtract number of base image layers
baseImageLayersCount=$(tar -xOf "${fromImage}" manifest.json | ${lib.getExe jq} '.[0].Layers | length')
(( usedLayers += baseImageLayersCount ))
''}
if ! (( $usedLayers < $maxLayers )); then
echo >&2 "Error: usedLayers $usedLayers layers to store 'fromImage' and" \
"'extraCommands', but only maxLayers=$maxLayers were" \
"allowed. At least 1 layer is required to store contents."
exit 1
fi
availableLayers=$(( maxLayers - usedLayers ))
# Produce pipeline which uses popularity_contest algo.
echo '[["popularity_contest"],["limit_layers",'$availableLayers']]' > $out
''
+2 -2
View File
@@ -18,14 +18,14 @@
python3Packages.buildPythonApplication rec {
pname = "alpaca";
version = "2.8.0";
version = "2.9.0";
pyproject = false; # Built with meson
src = fetchFromGitHub {
owner = "Jeffser";
repo = "Alpaca";
rev = "refs/tags/${version}";
hash = "sha256-Wk7a5/fO87yl/TolzoonSRLkc2TGEZKgVCgXcUuhjQI=";
hash = "sha256-ionioPA69haDIyXjqU84nuTNtI32jOnhd6oCTRI6vcA=";
};
nativeBuildInputs = [
+62
View File
@@ -0,0 +1,62 @@
{
python3Packages,
fetchFromGitHub,
lib,
}:
python3Packages.buildPythonApplication rec {
pname = "cruft";
version = "2.15.0";
pyproject = true;
src = fetchFromGitHub {
owner = "cruft";
repo = "cruft";
rev = version;
hash = "sha256-qIVyNMoI3LsoOV/6XPa60Y1vTRvkezesF7wF9WVSLGk=";
};
build-system = with python3Packages; [
poetry-core
];
nativeCheckInputs = with python3Packages; [
pytest7CheckHook
];
dependencies = with python3Packages; [
click
cookiecutter
gitpython
typer
];
pythonImportsCheck = "cruft";
# Unfortunately, some tests require internet access to fully clone
# https://github.com/cruft/cookiecutter-test (including all branches)
# which is possible to package, but is annoying and may be not always pure
#
# See https://discourse.nixos.org/t/keep-git-folder-in-when-fetching-a-git-repo/8590/6
#
# There are only 13 tests which do not require internet access on moment of the writing.
# But some tests are better than none, right?
disabledTests = [
"test_get_diff_with_add"
"test_get_diff_with_delete"
"test_get_diff_with_unicode"
];
disabledTestPaths = [
"tests/test_api.py" # only 2 tests pass, and 24 fail. I am going to ignore entire file
"tests/test_cli.py"
];
meta = {
changelog = "https://github.com/cruft/cruft/blob/${version}/CHANGELOG.md";
description = "Allows you to maintain all the necessary boilerplate for building projects";
homepage = "https://github.com/cruft/cruft";
license = lib.licenses.mit;
mainProgram = "cruft";
maintainers = with lib.maintainers; [ perchun ];
};
}
+62
View File
@@ -0,0 +1,62 @@
{
cwtch,
fetchgit,
flutter,
lib,
tor,
zenity,
}:
let
runtimeBinDependencies = [
tor
zenity
];
in
flutter.buildFlutterApplication rec {
pname = "cwtch-ui";
version = "1.15.1";
# This Gitea instance has archive downloads disabled, so: fetchgit
src = fetchgit {
url = "https://git.openprivacy.ca/cwtch.im/cwtch-ui";
rev = "v${version}";
hash = "sha256-+UtWhQMhm0UjY0kx3B5TwcBFhUfJma3rpeYls4XWy7I=";
};
# NOTE: The included pubspec.json does not exactly match the upstream
# pubspec.lock. With Flutter 3.24, a newer version of material_color_utilities
# is required than the upstream locked version. From a checkout of cwtch-ui
# 1.15.1, I ran `flutter pub upgrade material_color_utilities` on 2024-10-17.
# This upgraded material_color_utilities and its dependencies.
pubspecLock = lib.importJSON ./pubspec.json;
gitHashes = {
flutter_gherkin = "sha256-Y8tR84kkczQPBwh7cGhPFAAqrMZKRfGp/02huPaaQZg=";
};
flutterBuildFlags = [
"--dart-define"
"BUILD_VER=${version}"
"--dart-define"
"BUILD_DATE=1980-01-01-00:00"
];
# These things are added to LD_LIBRARY_PATH, but not PATH
runtimeDependencies = [ cwtch ];
extraWrapProgramArgs = "--prefix PATH : ${lib.makeBinPath runtimeBinDependencies}";
postInstall = ''
mkdir -p $out/share/applications
substitute linux/cwtch.template.desktop "$out/share/applications/cwtch.desktop" \
--replace-fail PREFIX "$out"
'';
meta = {
description = "Messaging app built on the cwtch decentralized, privacy-preserving, multi-party messaging protocol";
homepage = "https://cwtch.im/";
changelog = "https://docs.cwtch.im/changelog";
license = lib.licenses.mit;
mainProgram = "cwtch";
platforms = [ "x86_64-linux" ];
maintainers = [ lib.maintainers.gmacon ];
};
}
File diff suppressed because it is too large Load Diff
+52
View File
@@ -0,0 +1,52 @@
{
buildGoModule,
fetchgit,
lib,
}:
buildGoModule rec {
pname = "libcwtch";
version = "0.1.3";
# This Gitea instance has archive downloads disabled, so: fetchgit
src = fetchgit {
url = "https://git.openprivacy.ca/cwtch.im/autobindings.git";
rev = "v${version}";
hash = "sha256-Gp6JnyjKChB7w0fUHkreu81QRXQ3YdMb2ReD/VCkVOE=";
};
vendorHash = "sha256-UgG/yjupUFrfjPZ4E+OM1VsWi9MuMuHxcZ4yvz+q/Y0=";
overrideModAttrs = (
old: {
preBuild = ''
make lib.go
'';
}
);
postPatch = ''
substituteInPlace Makefile \
--replace-fail '$(shell git describe --tags)' v${version} \
--replace-fail '$(shell git log -1 --format=%cd --date=format:%G-%m-%d-%H-%M)' 1980-01-01-00-00
'';
buildPhase = ''
runHook preBuild
make linux
runHook postBuild
'';
installPhase = ''
runHook preInstall
install -D build/linux/libCwtch.h -t $out/include
install -D build/linux/libCwtch.*.so $out/lib/libCwtch.so
runHook postInstall
'';
meta = {
description = "Decentralized, privacy-preserving, multi-party messaging protocol";
homepage = "https://cwtch.im/";
changelog = "https://docs.cwtch.im/changelog";
license = lib.licenses.mit;
platforms = lib.platforms.linux;
maintainers = [ lib.maintainers.gmacon ];
};
}
@@ -27,7 +27,7 @@ buildNpmPackage rec {
homepage = "https://github.com/olrtg/emmet-language-server";
changelog = "https://github.com/olrtg/emmet-language-server/releases/tag/v${version}";
license = lib.licenses.mit;
maintainers = with lib.maintainers; [ stnley ];
maintainers = [ ];
mainProgram = "emmet-language-server";
};
}
+1 -1
View File
@@ -40,7 +40,7 @@ buildGoModule {
# Enabling silkworm also breaks the build as it requires dynamically linked libraries.
# If we need it in the future, we should consider packaging silkworm and silkworm-go
# as depenedencies explicitly.
tags = "-tags=nosqlite,noboltdb,nosilkworm";
tags = [ "nosqlite" "noboltdb" "nosilkworm" ];
passthru.updateScript = nix-update-script { };
@@ -0,0 +1,54 @@
# Start this shell with:
# nix-shell path/to/root/of/nixpkgs -A flattenReferencesGraph.dev-shell
{
mkShell,
callPackage,
python3Packages,
}:
let
helpers = callPackage (import ./helpers.nix) { };
in
mkShell {
inputsFrom = [ (callPackage (import ./package.nix) { }) ];
buildInputs = [
helpers.format
helpers.lint
helpers.unittest
# This is needed to plot graphs when DEBUG_PLOT is set to True.
python3Packages.pycairo
# This can be used on linux to display the graphs.
# On other platforms the image viewer needs to be set with
# DEBUG_PLOT_IMAGE_VIEWER env var.
# pkgs.gwenview
];
shellHook = ''
echo '
**********************************************************************
**********************************************************************
Commands useful for development (should be executed from scr dir):
format
* formats all files in place using autopep8
lint
* lints all files using flake8
unittest
* runs all unit tests
following env vars can be set to enable extra output in tests:
- DEBUG=True - enable debug logging
- DEBUG_PLOT=True - plot graphs processed by split_paths.py and
subcomponent.py
- DEBUG_PLOT_IMAGE_VIEWER=$PATH_OF_IMAGE_VIEWER_APP - app used to
display plots (default: gwenview)
- DEBUG_PLOT_SAVE_BASE_NAME=$SOME_NAME - if set, plots will be saved
to files instead of displayed with image viewer
**********************************************************************
**********************************************************************
'
'';
}
@@ -0,0 +1,36 @@
{
bash,
writers,
python3Packages,
}:
let
writeCheckedBashBin =
name:
let
interpreter = "${bash}/bin/bash";
in
writers.makeScriptWriter {
inherit interpreter;
check = "${interpreter} -n $1";
} "/bin/${name}";
# Helpers used during build/development.
lint = writeCheckedBashBin "lint" ''
${python3Packages.flake8}/bin/flake8 --show-source ''${@}
'';
unittest = writeCheckedBashBin "unittest" ''
if [ "$#" -eq 0 ]; then
set -- discover -p '*_test.py'
fi
${python3Packages.python}/bin/python -m unittest "''${@}"
'';
format = writeCheckedBashBin "format" ''
${python3Packages.autopep8}/bin/autopep8 -r -i . "''${@}"
'';
in
{
inherit format lint unittest;
}
@@ -0,0 +1,38 @@
{
callPackage,
lib,
python3Packages,
}:
let
inherit (lib) fileset;
helpers = callPackage ./helpers.nix { };
pythonPackages = python3Packages;
in
pythonPackages.buildPythonApplication {
version = "0.1.0";
pname = "flatten-references-graph";
src = fileset.toSource {
root = ./src;
fileset = fileset.unions [
./src/.flake8
./src/flatten_references_graph
./src/setup.py
];
};
propagatedBuildInputs = with pythonPackages; [
igraph
toolz
];
doCheck = true;
checkPhase = ''
${helpers.unittest}/bin/unittest
'';
passthru = {
dev-shell = callPackage ./dev-shell.nix { };
};
}
@@ -0,0 +1,4 @@
[flake8]
max-line-length = 80
[pep8]
aggressive = 1
@@ -0,0 +1 @@
__pycache__
@@ -0,0 +1,48 @@
import json as json
import sys as sys
from .lib import debug, load_json
from .flatten_references_graph import flatten_references_graph
def main_impl(file_path):
debug(f"loading json from {file_path}")
data = load_json(file_path)
# These are required
references_graph = data["graph"]
pipeline = data["pipeline"]
# This is optional
exclude_paths = data.get("exclude_paths")
debug("references_graph", references_graph)
debug("pipeline", pipeline)
debug("exclude_paths", exclude_paths)
result = flatten_references_graph(
references_graph,
pipeline,
exclude_paths=exclude_paths
)
debug("result", result)
return json.dumps(
result,
# For reproducibility.
sort_keys=True,
indent=2,
# Avoid tailing whitespaces.
separators=(",", ": ")
)
def main():
file_path = sys.argv[1]
print(main_impl(file_path))
if __name__ == "__main__":
main()
@@ -0,0 +1,52 @@
import unittest
import inspect as inspect
from .__main__ import main_impl
from .lib import path_relative_to_file
if __name__ == "__main__":
unittest.main()
class TestMain(unittest.TestCase):
def test_main_impl(self):
file_path = path_relative_to_file(
__file__,
"__test_fixtures/flatten-references-graph-main-input.json"
)
result = main_impl(file_path)
self.assertEqual(
result,
inspect.cleandoc(
"""
[
[
"B"
],
[
"C"
],
[
"A"
]
]
"""
)
)
def test_main_impl2(self):
file_path = path_relative_to_file(
__file__,
"__test_fixtures/flatten-references-graph-main-input-no-paths.json"
)
result = main_impl(file_path)
self.assertEqual(
result,
inspect.cleandoc("[]")
)
@@ -0,0 +1,31 @@
[
{
"closureSize": 1,
"narHash": "sha256:a",
"narSize": 2,
"path": "A",
"references": [
"A",
"B",
"C"
]
},
{
"closureSize": 3,
"narHash": "sha256:b",
"narSize": 4,
"path": "B",
"references": [
"C"
]
},
{
"closureSize": 5,
"narHash": "sha256:c",
"narSize": 6,
"path": "C",
"references": [
"C"
]
}
]
@@ -0,0 +1,23 @@
{
"graph": [
{
"closureSize": 168,
"narHash": "sha256:0dl4kfhb493yz8a5wgh0d2z3kr61z65gp85vx33rqwa1m1lnymy8",
"narSize": 168,
"path": "/nix/store/fakehash000000000000000000000000-no-store-paths-base.json",
"references": []
}
],
"pipeline": [
[
"popularity_contest"
],
[
"limit_layers",
99
]
],
"exclude_paths": [
"/nix/store/fakehash000000000000000000000000-no-store-paths-base.json"
]
}
@@ -0,0 +1,36 @@
{
"graph": [
{
"closureSize": 1,
"narHash": "sha256:a",
"narSize": 2,
"path": "A",
"references": [
"A",
"B",
"C"
]
},
{
"closureSize": 3,
"narHash": "sha256:b",
"narSize": 4,
"path": "B",
"references": [
"C"
]
},
{
"closureSize": 5,
"narHash": "sha256:c",
"narSize": 6,
"path": "C",
"references": [
"C"
]
}
],
"pipeline": [
["split_paths", ["B"]]
]
}
@@ -0,0 +1,45 @@
from toolz import curried as tlz
from .lib import (
flatten,
over,
references_graph_to_igraph
)
from .pipe import pipe
MAX_LAYERS = 127
def create_list_of_lists_of_strings(deeply_nested_lists_or_dicts_of_graphs):
list_of_graphs = flatten(deeply_nested_lists_or_dicts_of_graphs)
return list(
filter(
# remove empty layers
lambda xs: len(xs) > 0,
tlz.map(
lambda g: g.vs["name"],
list_of_graphs
)
)
)
def flatten_references_graph(references_graph, pipeline, exclude_paths=None):
if exclude_paths is not None:
exclude_paths = frozenset(exclude_paths)
references_graph = tlz.compose(
tlz.map(over(
"references",
lambda xs: frozenset(xs).difference(exclude_paths)
)),
tlz.remove(lambda node: node["path"] in exclude_paths)
)(references_graph)
igraph_graph = references_graph_to_igraph(references_graph)
return create_list_of_lists_of_strings(pipe(
pipeline,
igraph_graph
))
@@ -0,0 +1,121 @@
import unittest
from .flatten_references_graph import flatten_references_graph
# from .lib import path_relative_to_file, load_json
if __name__ == "__main__":
unittest.main()
references_graph = [
{
"closureSize": 1,
"narHash": "sha256:a",
"narSize": 2,
"path": "A",
"references": [
"A",
"C",
]
},
{
"closureSize": 3,
"narHash": "sha256:b",
"narSize": 4,
"path": "B",
"references": [
"C",
"D"
]
},
{
"closureSize": 5,
"narHash": "sha256:c",
"narSize": 6,
"path": "C",
"references": [
"C"
]
},
{
"closureSize": 7,
"narHash": "sha256:d",
"narSize": 8,
"path": "D",
"references": [
"D"
]
}
]
class Test(unittest.TestCase):
def test_flatten_references_graph(self):
pipeline = [
["split_paths", ["B"]],
]
result = flatten_references_graph(references_graph, pipeline)
self.assertEqual(
result,
[
# B and it's exclusive deps
["B", "D"],
# Common deps
["C"],
# Rest (without common deps)
["A"]
]
)
pipeline = [
["split_paths", ["B"]],
["over", "main", ["subcomponent_in", ["B"]]],
]
result = flatten_references_graph(references_graph, pipeline)
self.assertEqual(
result,
[
["B"],
["D"],
["C"],
["A"]
]
)
def test_flatten_references_graph_exclude_paths(self):
pipeline = [
["split_paths", ["B"]],
]
result = flatten_references_graph(
references_graph,
pipeline,
exclude_paths=["A"]
)
self.assertEqual(
result,
[
# A was excluded so there is no "rest" or "common" layer
["B", "C", "D"]
]
)
result = flatten_references_graph(
references_graph,
pipeline,
exclude_paths=["D"]
)
self.assertEqual(
result,
[
# D removed from this layer
["B"],
["C"],
["A"]
]
)
@@ -0,0 +1,329 @@
from collections.abc import Iterable
from pathlib import Path
from toolz import curried as tlz
from toolz import curry
import igraph as igraph
import itertools as itertools
import json as json
import os as os
import re as re
import sys
DEBUG = os.environ.get("DEBUG", False) == "True"
DEBUG_PLOT = os.environ.get("DEBUG_PLOT", False) == "True"
# If this is set, the plots will be saved to files instead of being displayed
# with default image viewer.
DEBUG_PLOT_SAVE_BASE_NAME = os.environ.get("DEBUG_PLOT_SAVE_BASE_NAME")
c = igraph.configuration.init()
# App used to open the plots when DEBUG_PLOT_SAVE_BASE_NAME is not set.
c["apps.image_viewer"] = os.environ.get("DEBUG_PLOT_IMAGE_VIEWER", "gwenview")
def debug(*args, **kwargs):
if DEBUG:
print(*args, file=sys.stderr, **kwargs)
def debug_plot(graph, name, **kwargs):
if not DEBUG_PLOT:
return
vertex_label = [
# remove /nix/store/HASH- prefix from labels
re.split("^/nix/store/[a-z0-9]{32}-", name)[-1]
for name in graph.vs["name"]
]
save_as = (
None if DEBUG_PLOT_SAVE_BASE_NAME is None
else DEBUG_PLOT_SAVE_BASE_NAME + name + ".png"
)
igraph.plot(
graph,
save_as,
vertex_label=vertex_label,
**(tlz.merge(
{
# "bbox": (3840, 2160),
"bbox": (800, 600),
"margin": 100,
"vertex_label_dist": -5,
"edge_color": "orange",
"vertex_size": 20,
"vertex_label_size": 30,
"edge_arrow_size": 2
},
kwargs
)),
)
def debug_plot_with_highligth(g, vs, layout):
debug_plot(
g,
layout=layout,
# layout=Layout(new_coords),
vertex_color=[
"green" if v.index in vs else "red"
for v in g.vs
]
)
@curry
def pick_keys(keys, d):
return {
key: d[key] for key in keys if key in d
}
def unnest_iterable(xs):
return itertools.chain.from_iterable(xs)
def load_json(file_path):
with open(file_path) as f:
return json.load(f)
@curry
def sorted_by(key, xs):
return sorted(xs, key=lambda x: x[key])
@curry
def find_vertex_by_name_or_none(graph, name):
try:
# NOTE: find by name is constant time.
return graph.vs.find(name)
# This will be thrown if vertex with given name is not found.
except ValueError:
return None
def subcomponent_multi(graph, vertices, mode="out"):
"""Return concatenated subcomponents generated by the given list of
vertices.
"""
return tlz.mapcat(
lambda vertex: graph.subcomponent(vertex, mode=mode),
vertices
)
@curry
def edges_for_reference_graph_node(path_to_size_dict, reference_graph_node):
source = reference_graph_node["path"]
return map(
lambda x: {"source": source, "target": x},
sorted(
filter(
# references might contain source
lambda x: x != source,
reference_graph_node["references"]
),
key=lambda x: 1 * path_to_size_dict[x]
)
)
reference_graph_node_keys_to_keep = [
"closureSize",
"narSize"
]
pick_reference_graph_node_keys = pick_keys(reference_graph_node_keys_to_keep)
def vertex_from_reference_graph_node(reference_graph_node):
return tlz.merge(
{"name": reference_graph_node["path"]},
pick_reference_graph_node_keys(reference_graph_node)
)
def references_graph_to_igraph(references_graph):
"""
Converts result of exportReferencesGraph into an igraph directed graph.
Uses paths as igraph node names, and sets closureSize and narSize as
properties of igraph nodes.
"""
debug('references_graph', references_graph)
references_graph = sorted(references_graph, key=lambda x: 1 * x["narSize"])
# Short circuit since DictList throws an error if first argument (vertices)
# contains no elements.
# The error is: KeyError: 'name'
# here: https://github.com/igraph/python-igraph/blob/da7484807f5152a2c18c55dd4154653de2c7f5f7/src/igraph/__init__.py#L3091 # noqa: E501
# This looks like a bug.
if len(references_graph) == 0:
return empty_directed_graph()
path_to_size_dict = {
node["path"]: node["narSize"] for node in references_graph
}
debug('path_to_size_dict', path_to_size_dict)
return igraph.Graph.DictList(
map(vertex_from_reference_graph_node, references_graph),
unnest_iterable(map(
edges_for_reference_graph_node(path_to_size_dict),
references_graph
)),
directed=True
)
@curry
def graph_vertex_index_to_name(graph, index):
return graph.vs[index]["name"]
def igraph_to_reference_graph(igraph_instance):
return [
tlz.merge(
{
"path": v["name"],
"references": list(map(
graph_vertex_index_to_name(igraph_instance),
igraph_instance.successors(v.index)
))
},
pick_reference_graph_node_keys(v.attributes())
)
for v in igraph_instance.vs
]
def load_closure_graph(file_path):
return references_graph_to_igraph(load_json(file_path))
def path_relative_to_file(file_path_from, file_path):
dir_path = Path(file_path_from).parent
return dir_path / file_path
def is_None(x):
return x is None
def not_None(x):
return x is not None
def print_layers(layers):
debug("\n::::LAYERS:::::")
for index, layer in enumerate(layers):
debug("")
debug("layer index:", index)
debug("[")
for v in layer.vs["name"]:
debug(" ", v)
debug("]")
def print_vs(graph):
for v in graph.vs:
debug(v)
def directed_graph(edges, vertices=None, vertex_attrs=[]):
graph = igraph.Graph.TupleList(edges, directed=True)
# Add detached vertices (without edges) if any.
if vertices is not None:
graph = graph + vertices
# Add vertex attributes if any.
for (name, attrs_dict) in vertex_attrs:
vertex = graph.vs.find(name)
for (k, v) in attrs_dict.items():
vertex[k] = v
return graph
def empty_directed_graph():
return directed_graph([])
def graph_is_empty(graph):
return len(graph.vs) == 0
def pick_attrs(attrs, x):
return {attr: getattr(x, attr) for attr in attrs}
def merge_graphs(graphs):
return tlz.reduce(lambda acc, g: acc + g, graphs, empty_directed_graph())
# Functions below can be used in user defined pipeline (see pipe.py).
# All functions need to be curried, and the user needs to be able to
# provide values for all arguments apart from the last one from nix code.
@curry
def over(prop_name, func, dictionary):
value = dictionary[prop_name]
return tlz.assoc(dictionary, prop_name, func(value))
# One argument functions also need to be curried to simplify processing of the
# pipeline.
@curry
def flatten(xs):
xs = xs.values() if isinstance(xs, dict) else xs
for x in xs:
if isinstance(x, Iterable) and not isinstance(x, (str, bytes)):
yield from flatten(x)
else:
yield x
@curry
def split_every(count, graph):
vs = graph.vs
return [
graph.induced_subgraph(vs[x:x + count])
for x in range(0, len(vs), count)
]
@curry
def limit_layers(max_count, graphs):
assert max_count > 0, "max count needs to > 0"
graphs_iterator = iter(graphs)
return tlz.concat([
tlz.take(max_count - 1, graphs_iterator),
# Merges all graphs remaining in the iterator, after initial
# max_count - 1 have been taken.
(lambda: (yield merge_graphs(graphs_iterator)))()
])
@curry
def remove_paths(paths, graph):
# Allow passing a single path.
if isinstance(paths, str):
paths = [paths]
indices_to_remove = tlz.compose(
list,
tlz.map(lambda v: v.index),
tlz.remove(is_None),
tlz.map(find_vertex_by_name_or_none(graph))
)(paths)
return graph - indices_to_remove if len(indices_to_remove) > 0 else graph
@curry
def reverse(iterator):
return reversed(list(iterator))
@@ -0,0 +1,199 @@
import unittest
from toolz import curried as tlz
from . import test_helpers as th
from .lib import (
directed_graph,
igraph_to_reference_graph,
limit_layers,
pick_keys,
references_graph_to_igraph,
reference_graph_node_keys_to_keep
)
if __name__ == "__main__":
unittest.main()
references_graph = [
{
"closureSize": 3,
"narHash": "sha256:d",
"narSize": 0,
"path": "D",
"references": [
"D"
]
},
{
"closureSize": 3,
"narHash": "sha256:b",
"narSize": 4,
"path": "B",
"references": [
"B"
]
},
{
"closureSize": 3,
"narHash": "sha256:e",
"narSize": 5,
"path": "E",
"references": [
"E"
]
},
{
"closureSize": 1,
"narHash": "sha256:a",
"narSize": 10,
"path": "A",
"references": [
# most of the time references contain self path, but not always.
"C",
"B",
]
},
{
"closureSize": 5,
"narHash": "sha256:c",
"narSize": 6,
"path": "C",
"references": [
"C",
"E",
"D"
]
},
{
"closureSize": 5,
"narHash": "sha256:f",
"narSize": 2,
"path": "F",
"references": [
"F"
]
}
]
class TestLib(unittest.TestCase, th.CustomAssertions):
def test_references_graph_to_igraph(self):
graph = references_graph_to_igraph(references_graph)
pick_preserved_keys = pick_keys(reference_graph_node_keys_to_keep)
self.assertGraphEqual(
graph,
directed_graph(
[
("A", "B"),
("A", "C"),
("C", "E"),
("C", "D"),
],
["F"],
# Add "narSize" and "closureSize" attributes to each node.
map(
lambda node: (node["path"], pick_preserved_keys(node)),
references_graph
)
)
)
def test_references_graph_to_igraph_one_node(self):
references_graph = [
{
'closureSize': 168,
'narHash': 'sha256:0dl4',
'narSize': 168,
'path': 'A',
'references': []
}
]
graph = references_graph_to_igraph(references_graph)
pick_preserved_keys = pick_keys(reference_graph_node_keys_to_keep)
self.assertGraphEqual(
graph,
directed_graph(
[],
["A"],
# Add "narSize" and "closureSize" attributes to each node.
map(
lambda node: (node["path"], pick_preserved_keys(node)),
references_graph
)
)
)
def test_references_graph_to_igraph_zero_nodes(self):
references_graph = []
graph = references_graph_to_igraph(references_graph)
self.assertGraphEqual(
graph,
directed_graph(
[],
[],
[]
)
)
def test_igraph_to_reference_graph(self):
graph = references_graph_to_igraph(references_graph)
nodes_by_path = {
node["path"]: node for node in references_graph
}
result = igraph_to_reference_graph(graph)
self.assertEqual(
len(result),
len(references_graph)
)
pick_preserved_keys = pick_keys([
"path",
*reference_graph_node_keys_to_keep
])
for node in result:
original_node = nodes_by_path[node["path"]]
self.assertDictEqual(
pick_preserved_keys(original_node),
pick_preserved_keys(node)
)
revove_self_ref = tlz.remove(lambda a: a == node["path"])
self.assertListEqual(
sorted(node["references"]),
sorted(revove_self_ref(original_node["references"]))
)
def test_limit_layers_nothing_to_do(self):
graph = references_graph_to_igraph(references_graph)
layers = [graph]
result = limit_layers(1, layers)
result_list = list(result)
self.assertEqual(
len(result_list),
1
)
self.assertGraphEqual(graph, result_list[0])
@@ -0,0 +1,80 @@
from toolz import curried as tlz
from toolz import curry
from . import lib as lib
from . import subcomponent as subcomponent
from .popularity_contest import popularity_contest
from .split_paths import split_paths
from .lib import (
# references_graph_to_igraph
debug,
pick_attrs
)
funcs = tlz.merge(
pick_attrs(
[
"flatten",
"over",
"split_every",
"limit_layers",
"remove_paths",
"reverse"
],
lib
),
pick_attrs(
[
"subcomponent_in",
"subcomponent_out",
],
subcomponent
),
{
"split_paths": split_paths,
"popularity_contest": popularity_contest,
"map": tlz.map
}
)
@curry
def nth_or_none(index, xs):
try:
return xs[index]
except IndexError:
return None
def preapply_func(func_call_data):
[func_name, *args] = func_call_data
debug("func_name", func_name)
debug("args", args)
debug('func_name in ["over"]', func_name in ["over"])
# TODO: these could be handled in more generic way by defining, for each
# function, which of the args are expected to be functions which need
# pre-applying.
if func_name == "over":
[first_arg, second_arg] = args
args = [first_arg, preapply_func(second_arg)]
elif func_name == "map":
args = [preapply_func(args[0])]
return funcs[func_name](*args)
@curry
def pipe(pipeline, data):
debug("pipeline", pipeline)
partial_funcs = list(tlz.map(preapply_func, pipeline))
debug('partial_funcs', partial_funcs)
return tlz.pipe(
data,
*partial_funcs
)
funcs["pipe"] = pipe
@@ -0,0 +1,153 @@
import unittest
from .pipe import pipe
from . import test_helpers as th
from .lib import (
directed_graph,
)
if __name__ == "__main__":
unittest.main()
def make_test_graph():
edges = [
("Root1", "A"),
("A", "B"),
("A", "C"),
("B", "D"),
("B", "E"),
("E", "F"),
("B", "G"),
("Root2", "B"),
("Root3", "C"),
]
return directed_graph(edges)
class CustomAssertions:
def runAndAssertResult(self, graph, pipeline, expected_graph_args):
result = list(pipe(pipeline, graph))
for (index, expected_graph_arg) in enumerate(expected_graph_args):
self.assertGraphEqual(
directed_graph(*expected_graph_arg),
result[index]
)
if __name__ == "__main__":
unittest.main()
class Test(
unittest.TestCase,
CustomAssertions,
th.CustomAssertions
):
def test_1(self):
pipeline = [
["split_paths", ["B"]],
[
"over",
"main",
[
"pipe",
[
["subcomponent_in", ["B"]],
[
"over",
"rest",
["popularity_contest"]
]
]
]
],
["flatten"],
["map", ["remove_paths", "Root3"]],
["limit_layers", 5],
]
expected_graph_args = [
# "B"" separated from the rest by "split_paths" and
# "subcomponent_in' stages.
([], ["B"]),
# Deps of "B", split into individual layers by "popularity_contest",
# with "F" being most popular
([], ["F"]),
([], ["D"]),
([], ["E"]),
# "rest" output of "split_paths" stage with "G" merged into it by
# "limit_layers" stage.
(
[
("Root1", "A"),
("A", "C")
],
["Root2", "G"]
)
]
self.runAndAssertResult(
make_test_graph(),
pipeline,
expected_graph_args
)
def test_2(self):
graph = directed_graph(
[
("Root1", "A"),
("A", "B"),
],
["Root2"]
)
self.runAndAssertResult(
graph,
[
["popularity_contest"],
],
[
# Ordered from most to least popular
([], ["B"]),
([], ["A"]),
([], ["Root1"]),
([], ["Root2"])
]
)
self.runAndAssertResult(
graph,
[
["popularity_contest"],
["limit_layers", 3],
],
[
# Most popular first
([], ["B"]),
([], ["A"]),
# Least popular combined
([], ["Root1", "Root2"]),
]
)
self.runAndAssertResult(
graph,
[
["popularity_contest"],
["reverse"],
["limit_layers", 3],
],
[
# Least popular first
([], ["Root2"]),
([], ["Root1"]),
# Most popular first
([], ["A", "B"])
]
)
@@ -0,0 +1,398 @@
# Using a simple algorithm, convert the references to a path in to a
# sorted list of dependent paths based on how often they're referenced
# and how deep in the tree they live. Equally-"popular" paths are then
# sorted by name.
#
# The existing writeReferencesToFile prints the paths in a simple
# ascii-based sorting of the paths.
#
# Sorting the paths by graph improves the chances that the difference
# between two builds appear near the end of the list, instead of near
# the beginning. This makes a difference for Nix builds which export a
# closure for another program to consume, if that program implements its
# own level of binary diffing.
#
# For an example, Docker Images. If each store path is a separate layer
# then Docker Images can be very efficiently transfered between systems,
# and we get very good cache reuse between images built with the same
# version of Nixpkgs. However, since Docker only reliably supports a
# small number of layers (42) it is important to pick the individual
# layers carefully. By storing very popular store paths in the first 40
# layers, we improve the chances that the next Docker image will share
# many of those layers.*
#
# Given the dependency tree:
#
# A - B - C - D -\
# \ \ \ \
# \ \ \ \
# \ \ - E ---- F
# \- G
#
# Nodes which have multiple references are duplicated:
#
# A - B - C - D - F
# \ \ \
# \ \ \- E - F
# \ \
# \ \- E - F
# \
# \- G
#
# Each leaf node is now replaced by a counter defaulted to 1:
#
# A - B - C - D - (F:1)
# \ \ \
# \ \ \- E - (F:1)
# \ \
# \ \- E - (F:1)
# \
# \- (G:1)
#
# Then each leaf counter is merged with its parent node, replacing the
# parent node with a counter of 1, and each existing counter being
# incremented by 1. That is to say `- D - (F:1)` becomes `- (D:1, F:2)`:
#
# A - B - C - (D:1, F:2)
# \ \ \
# \ \ \- (E:1, F:2)
# \ \
# \ \- (E:1, F:2)
# \
# \- (G:1)
#
# Then each leaf counter is merged with its parent node again, merging
# any counters, then incrementing each:
#
# A - B - (C:1, D:2, E:2, F:5)
# \ \
# \ \- (E:1, F:2)
# \
# \- (G:1)
#
# And again:
#
# A - (B:1, C:2, D:3, E:4, F:8)
# \
# \- (G:1)
#
# And again:
#
# (A:1, B:2, C:3, D:4, E:5, F:9, G:2)
#
# and then paths have the following "popularity":
#
# A 1
# B 2
# C 3
# D 4
# E 5
# F 9
# G 2
#
# and the popularity contest would result in the paths being printed as:
#
# F
# E
# D
# C
# B
# G
# A
#
# * Note: People who have used a Dockerfile before assume Docker's
# Layers are inherently ordered. However, this is not true -- Docker
# layers are content-addressable and are not explicitly layered until
# they are composed in to an Image.
import igraph as igraph
from collections import defaultdict
from operator import eq
from toolz import curried as tlz
from toolz import curry
from .lib import (
debug,
directed_graph,
igraph_to_reference_graph,
over,
pick_keys,
reference_graph_node_keys_to_keep
)
eq = curry(eq)
pick_keys_to_keep = pick_keys(reference_graph_node_keys_to_keep)
# Find paths in the original dataset which are never referenced by
# any other paths
def find_roots(closures):
debug('closures', closures)
roots = []
for closure in closures:
path = closure['path']
if not any_refer_to(path, closures):
roots.append(path)
return roots
def any_refer_to(path, closures):
for closure in closures:
if path != closure['path']:
if path in closure['references']:
return True
return False
def all_paths(closures):
paths = []
for closure in closures:
paths.append(closure['path'])
paths.extend(closure['references'])
paths.sort()
return list(set(paths))
# Convert:
#
# [
# { path: /nix/store/foo, references: [ /nix/store/foo, /nix/store/bar, /nix/store/baz ] }, # noqa: E501
# { path: /nix/store/bar, references: [ /nix/store/bar, /nix/store/baz ] },
# { path: /nix/store/baz, references: [ /nix/store/baz, /nix/store/tux ] },
# { path: /nix/store/tux, references: [ /nix/store/tux ] }
# ]
#
# To:
# {
# /nix/store/foo: [ /nix/store/bar, /nix/store/baz ],
# /nix/store/bar: [ /nix/store/baz ],
# /nix/store/baz: [ /nix/store/tux ] },
# /nix/store/tux: [ ]
# }
#
# Note that it drops self-references to avoid loops.
def make_lookup(closures):
return {
# remove self reference
node["path"]: over("references", tlz.remove(eq(node["path"])), node)
for node in closures
}
# Convert:
#
# /nix/store/foo with
# {
# /nix/store/foo: [ /nix/store/bar, /nix/store/baz ],
# /nix/store/bar: [ /nix/store/baz ],
# /nix/store/baz: [ /nix/store/tux ] },
# /nix/store/tux: [ ]
# }
#
# To:
#
# {
# /nix/store/bar: {
# /nix/store/baz: {
# /nix/store/tux: {}
# }
# },
# /nix/store/baz: {
# /nix/store/tux: {}
# }
# }
def make_graph_segment_from_root(subgraphs_cache, root, lookup):
children = {}
for ref in lookup[root]:
# make_graph_segment_from_root is a pure function, and will
# always return the same result based on a given input. Thus,
# cache computation.
#
# Python's assignment will use a pointer, preventing memory
# bloat for large graphs.
if ref not in subgraphs_cache:
debug("Subgraph Cache miss on {}".format(ref))
subgraphs_cache[ref] = make_graph_segment_from_root(
subgraphs_cache, ref, lookup
)
else:
debug("Subgraph Cache hit on {}".format(ref))
children[ref] = subgraphs_cache[ref]
return children
# Convert a graph segment in to a popularity-counted dictionary:
#
# From:
# {
# /nix/store/foo: {
# /nix/store/bar: {
# /nix/store/baz: {
# /nix/store/tux: {}
# }
# }
# /nix/store/baz: {
# /nix/store/tux: {}
# }
# }
# }
#
# to:
# [
# /nix/store/foo: 1
# /nix/store/bar: 2
# /nix/store/baz: 4
# /nix/store/tux: 6
# ]
def graph_popularity_contest(popularity_cache, full_graph):
popularity = defaultdict(int)
for path, subgraph in full_graph.items():
popularity[path] += 1
# graph_popularity_contest is a pure function, and will
# always return the same result based on a given input. Thus,
# cache computation.
#
# Python's assignment will use a pointer, preventing memory
# bloat for large graphs.
if path not in popularity_cache:
debug("Popularity Cache miss on", path)
popularity_cache[path] = graph_popularity_contest(
popularity_cache, subgraph
)
else:
debug("Popularity Cache hit on", path)
subcontest = popularity_cache[path]
for subpath, subpopularity in subcontest.items():
debug("Calculating popularity for", subpath)
popularity[subpath] += subpopularity + 1
return popularity
# Emit a list of packages by popularity, most first:
#
# From:
# [
# /nix/store/foo: 1
# /nix/store/bar: 1
# /nix/store/baz: 2
# /nix/store/tux: 2
# ]
#
# To:
# [ /nix/store/baz /nix/store/tux /nix/store/bar /nix/store/foo ]
def order_by_popularity(paths):
paths_by_popularity = defaultdict(list)
popularities = []
for path, popularity in paths.items():
popularities.append(popularity)
paths_by_popularity[popularity].append(path)
popularities = sorted(set(popularities))
flat_ordered = []
for popularity in popularities:
paths = paths_by_popularity[popularity]
paths.sort(key=package_name)
flat_ordered.extend(reversed(paths))
return list(reversed(flat_ordered))
def package_name(path):
parts = path.split('-')
start = parts.pop(0)
# don't throw away any data, so the order is always the same.
# even in cases where only the hash at the start has changed.
parts.append(start)
return '-'.join(parts)
@curry
def popularity_contest(graph):
# Data comes in as an igraph directed graph or in the format produced
# by nix's exportReferencesGraph:
# [
# { path: /nix/store/foo, references: [ /nix/store/foo, /nix/store/bar, /nix/store/baz ] }, # noqa: E501
# { path: /nix/store/bar, references: [ /nix/store/bar, /nix/store/baz ] }, # noqa: E501
# { path: /nix/store/baz, references: [ /nix/store/baz, /nix/store/tux ] }, # noqa: E501
# { path: /nix/store/tux, references: [ /nix/store/tux ] }
# ]
#
# We want to get out a list of paths ordered by how universally,
# important they are, ie: tux is referenced by every path, transitively
# so it should be #1
#
# [
# /nix/store/tux,
# /nix/store/baz,
# /nix/store/bar,
# /nix/store/foo,
# ]
#
# NOTE: the output is actually a list of igraph graphs with a single vertex
# with v["name"] == path, and some properties (defined in
# reference_graph_node_keys_to_keep) from the nodes of the input graph
# copied as vertex attributes.
debug('graph', graph)
if isinstance(graph, igraph.Graph):
graph = igraph_to_reference_graph(graph)
debug("Finding roots")
roots = find_roots(graph)
debug("Making lookup")
lookup = make_lookup(graph)
full_graph = {}
subgraphs_cache = {}
for root in roots:
debug("Making full graph for", root)
full_graph[root] = make_graph_segment_from_root(
subgraphs_cache,
root,
tlz.valmap(
tlz.get("references"),
lookup
)
)
debug("Running contest")
contest = graph_popularity_contest({}, full_graph)
debug("Ordering by popularity")
ordered = order_by_popularity(contest)
debug("Checking for missing paths")
missing = []
for path in all_paths(graph):
if path not in ordered:
missing.append(path)
ordered.extend(missing)
return map(
# Turn each path into a graph with 1 vertex.
lambda path: directed_graph(
# No edges
[],
# One vertex, with name=path
[path],
# Setting desired attributes on the vertex.
[(path, pick_keys_to_keep(lookup[path]))]
),
ordered
)
@@ -0,0 +1,335 @@
import unittest
from toolz import curry
from toolz import curried as tlz
from . import test_helpers as th
from .popularity_contest import (
all_paths,
any_refer_to,
find_roots,
graph_popularity_contest,
make_graph_segment_from_root,
make_lookup,
popularity_contest,
order_by_popularity
)
from .lib import (
directed_graph,
igraph_to_reference_graph,
over
)
if __name__ == "__main__":
unittest.main()
class CustomAssertions:
@curry
def assertResultKeys(self, keys, result):
self.assertListEqual(
list(result.keys()),
keys
)
return result
class Test(
unittest.TestCase,
CustomAssertions,
th.CustomAssertions
):
def test_empty_graph(self):
def test_empty(graph):
self.assertListEqual(
list(popularity_contest(graph)),
[]
)
# popularity_contest works with igraph graph or refurence_graph in
# form a list of dicts (as returned by nix's exportReferencesGraph)
test_empty(directed_graph([]))
test_empty([])
def test_popularity_contest(self):
# Making sure vertex attrs are preserved.
vertex_props_dict = {
"Root1": {"narSize": 1, "closureSize": 2},
"B": {"narSize": 3, "closureSize": 4},
"X": {"narSize": 5, "closureSize": 6},
}
edges = [
("Root1", "A"),
("A", "B"),
("A", "D"),
("D", "E"),
("B", "D"),
("B", "F"),
("Root2", "B"),
("Root3", "C")
]
detached_vertices = ["X"]
vertex_props = vertex_props_dict.items()
def test(graph):
result = list(popularity_contest(graph))
expected_paths = [
'E',
'D',
'F',
'B',
'A',
'C',
'Root1',
'Root2',
'Root3',
'X'
]
self.assertEqual(
len(result),
len(expected_paths)
)
for (index, path) in enumerate(expected_paths):
path_props = vertex_props_dict.get(path) or {}
self.assertGraphEqual(
result[index],
directed_graph([], [path], [(path, path_props)])
)
graph = directed_graph(edges, detached_vertices, vertex_props)
test(graph)
test(igraph_to_reference_graph(graph))
class TestFindRoots(unittest.TestCase):
def test_find_roots(self):
self.assertCountEqual(
find_roots([
{
"path": "/nix/store/foo",
"references": [
"/nix/store/foo",
"/nix/store/bar"
]
},
{
"path": "/nix/store/bar",
"references": [
"/nix/store/bar",
"/nix/store/tux"
]
},
{
"path": "/nix/store/hello",
"references": [
]
}
]),
["/nix/store/foo", "/nix/store/hello"]
)
class TestAnyReferTo(unittest.TestCase):
def test_has_references(self):
self.assertTrue(
any_refer_to(
"/nix/store/bar",
[
{
"path": "/nix/store/foo",
"references": [
"/nix/store/bar"
]
},
]
),
)
def test_no_references(self):
self.assertFalse(
any_refer_to(
"/nix/store/foo",
[
{
"path": "/nix/store/foo",
"references": [
"/nix/store/foo",
"/nix/store/bar"
]
},
]
),
)
class TestAllPaths(unittest.TestCase):
def test_returns_all_paths(self):
self.assertCountEqual(
all_paths([
{
"path": "/nix/store/foo",
"references": [
"/nix/store/foo",
"/nix/store/bar"
]
},
{
"path": "/nix/store/bar",
"references": [
"/nix/store/bar",
"/nix/store/tux"
]
},
{
"path": "/nix/store/hello",
"references": [
]
}
]),
["/nix/store/foo", "/nix/store/bar",
"/nix/store/hello", "/nix/store/tux", ]
)
def test_no_references(self):
self.assertFalse(
any_refer_to(
"/nix/store/foo",
[
{
"path": "/nix/store/foo",
"references": [
"/nix/store/foo",
"/nix/store/bar"
]
},
]
),
)
class TestMakeLookup(unittest.TestCase):
def test_returns_lookp(self):
self.assertDictEqual(
# "references" in the result are iterators so we need
# to convert them to a list before asserting.
tlz.valmap(over("references", list), make_lookup([
{
"path": "/nix/store/foo",
"references": [
"/nix/store/foo",
"/nix/store/bar",
"/nix/store/hello"
]
},
{
"path": "/nix/store/bar",
"references": [
"/nix/store/bar",
"/nix/store/tux"
]
},
{
"path": "/nix/store/hello",
"references": [
]
}
])),
{
"/nix/store/foo": {
"path": "/nix/store/foo",
"references": [
"/nix/store/bar",
"/nix/store/hello"
]
},
"/nix/store/bar": {
"path": "/nix/store/bar",
"references": [
"/nix/store/tux"
]
},
"/nix/store/hello": {
"path": "/nix/store/hello",
"references": [
]
}
}
)
class TestMakeGraphSegmentFromRoot(unittest.TestCase):
def test_returns_graph(self):
self.assertDictEqual(
make_graph_segment_from_root({}, "/nix/store/foo", {
"/nix/store/foo": ["/nix/store/bar"],
"/nix/store/bar": ["/nix/store/tux"],
"/nix/store/tux": [],
"/nix/store/hello": [],
}),
{
"/nix/store/bar": {
"/nix/store/tux": {}
}
}
)
def test_returns_graph_tiny(self):
self.assertDictEqual(
make_graph_segment_from_root({}, "/nix/store/tux", {
"/nix/store/foo": ["/nix/store/bar"],
"/nix/store/bar": ["/nix/store/tux"],
"/nix/store/tux": [],
}),
{}
)
class TestGraphPopularityContest(unittest.TestCase):
def test_counts_popularity(self):
self.assertDictEqual(
graph_popularity_contest({}, {
"/nix/store/foo": {
"/nix/store/bar": {
"/nix/store/baz": {
"/nix/store/tux": {}
}
},
"/nix/store/baz": {
"/nix/store/tux": {}
}
}
}),
{
"/nix/store/foo": 1,
"/nix/store/bar": 2,
"/nix/store/baz": 4,
"/nix/store/tux": 6,
}
)
class TestOrderByPopularity(unittest.TestCase):
def test_returns_in_order(self):
self.assertEqual(
order_by_popularity({
"/nix/store/foo": 1,
"/nix/store/bar": 1,
"/nix/store/baz": 2,
"/nix/store/tux": 2,
}),
[
"/nix/store/baz",
"/nix/store/tux",
"/nix/store/bar",
"/nix/store/foo"
]
)
@@ -0,0 +1,227 @@
from toolz import curried as tlz
from toolz import curry
from .lib import (
debug,
debug_plot,
DEBUG_PLOT,
find_vertex_by_name_or_none,
graph_is_empty,
is_None,
subcomponent_multi,
unnest_iterable
)
@curry
def coerce_to_singly_rooted_graph(fake_root_name, graph):
"""Add single root to the graph connected to all existing roots.
If graph has only one root, return the graph unchanged and the name
of the root vertex.
Otherwise return a modified graph (copy) and a name of the added root
vertex.
"""
roots = graph.vs.select(lambda v: len(graph.predecessors(v)) == 0)
root_names = roots["name"]
if len(root_names) == 1:
return graph, root_names[0]
else:
edges = [(fake_root_name, v) for v in root_names]
graph_with_root = graph + fake_root_name + edges
return graph_with_root, fake_root_name
@curry
def remove_vertex(vertex_name, graph):
"""Remove vertex with given name, returning copy of input graph if vertex
with given name is found in the graph
"""
vertex = find_vertex_by_name_or_none(graph)(vertex_name)
return graph - vertex_name if vertex else graph
def get_children_of(graph, vertex_names):
return unnest_iterable(map(
graph.successors,
tlz.remove(
is_None,
map(
find_vertex_by_name_or_none(graph),
vertex_names
)
)
))
def as_list(x):
return x if isinstance(x, list) else [x]
@curry
def split_path_spec_to_indices(graph, split_path_spec):
debug("split_path_spec", split_path_spec)
if isinstance(split_path_spec, dict):
if "children_of" in split_path_spec:
children_of = split_path_spec["children_of"]
return get_children_of(graph, as_list(children_of))
else:
raise Exception(
"Unexpected split path spec: dict with invalid keys."
"Valid: [\"children_of\"]"
)
else:
vertex = find_vertex_by_name_or_none(graph)(split_path_spec)
return [] if is_None(vertex) else [vertex.index]
call_count = 0
@curry
def split_paths(split_paths, graph_in):
debug("____")
debug("split_paths:", split_paths)
debug("graph_in:", graph_in)
if DEBUG_PLOT:
global call_count
graph_name_prefix = f"split_paths_{call_count}_"
call_count += 1
# Convert list of split_paths into list of vertex indices. Ignores
# split_paths which don"t match any vertices in the graph.
# All edges pointing at the indices will be deleted from the graph.
split_path_indices = list(unnest_iterable(map(
split_path_spec_to_indices(graph_in),
split_paths
)))
debug("split_path_indices:", split_path_indices)
# Short circuit if there is nothing to do (split_paths didn"t match any
# vertices in the graph).
if len(split_path_indices) == 0:
if DEBUG_PLOT:
layout = graph_in.layout('tree')
debug_plot(graph_in, f"{graph_name_prefix}input", layout=layout)
debug_plot(graph_in, f"{graph_name_prefix}result", layout=layout)
return {"rest": graph_in}
# If graph has multiple roots, add a single one connecting all existing
# roots to make it easy to split the graph into 2 sets of vertices after
# deleting edges pointing at split_path_indices.
fake_root_name = "__root__"
graph, root_name = coerce_to_singly_rooted_graph(fake_root_name, graph_in)
debug("root_name", root_name)
if (
find_vertex_by_name_or_none(graph)(root_name).index
in split_path_indices
):
if DEBUG_PLOT:
layout = graph_in.layout('tree')
debug_plot(graph_in, f"{graph_name_prefix}input", layout=layout)
debug_plot(
graph_in,
f"{graph_name_prefix}result",
layout=layout,
vertex_color="green"
)
return {"main": graph_in}
# Copy graph if coerce_to_singly_rooted_graph has not already created
# a copy, since we are going to mutate the graph and don"t want to
# mutate a function argument.
graph = graph if graph is not graph_in else graph.copy()
if DEBUG_PLOT:
layout = graph.layout('tree')
debug_plot(graph, f"{graph_name_prefix}input", layout=layout)
# Get incidences of all vertices which can be reached split_path_indices
# (including split_path_indices). This is a set of all split_paths and their
# dependencies.
split_off_vertex_indices = frozenset(
subcomponent_multi(graph, split_path_indices))
debug("split_off_vertex_indices", split_off_vertex_indices)
# Delete edges which point at any of the vertices in split_path_indices.
graph.delete_edges(_target_in=split_path_indices)
if DEBUG_PLOT:
debug_plot(graph, f"{graph_name_prefix}deleted_edges", layout=layout)
# Get incidences of all vertices which can be reached from the root. Since
# edges pointing at split_path_indices have been deleted, none of the
# split_path_indices will be included. Dependencies of rest_with_common will
# only be included if they can be reached from any vertex which is itself
# not in split_off_vertex_indices.
rest_with_common = frozenset(graph.subcomponent(root_name, mode="out"))
debug("rest_with_common", rest_with_common)
# Get a set of all dependencies common to split_path_indices and the rest
# of the graph.
common = split_off_vertex_indices.intersection(rest_with_common)
debug("common", common)
# Get a set of vertices which cannot be reached from split_path_indices.
rest_without_common = rest_with_common.difference(common)
debug("rest_without_common", rest_without_common)
# Get a set of split_path_indices and their dependencies which cannot be
# reached from the rest of the graph.
split_off_without_common = split_off_vertex_indices.difference(common)
debug("split_off_without_common", split_off_without_common)
if DEBUG_PLOT:
def choose_color(index):
if (index in split_off_without_common):
return "green"
elif (index in rest_without_common):
return "red"
else:
return "purple"
vertex_color = [choose_color(v.index) for v in graph.vs]
debug_plot(
graph,
f"{graph_name_prefix}result",
layout=layout,
vertex_color=vertex_color
)
# Return subgraphs based on calculated sets of vertices.
result_keys = ["main", "common", "rest"]
result_values = [
# Split paths and their deps (unreachable from rest of the graph).
graph.induced_subgraph(split_off_without_common),
# Dependencies of split paths which can be reached from the rest of the
# graph.
graph.induced_subgraph(common),
# Rest of the graph (without dependencies common with split paths).
graph.induced_subgraph(rest_without_common),
]
debug('result_values', result_values[0].vs["name"])
return tlz.valfilter(
tlz.complement(graph_is_empty),
dict(zip(
result_keys,
(
result_values if root_name != fake_root_name
# If root was added, remove it
else tlz.map(remove_vertex(fake_root_name), result_values)
)
))
)
@@ -0,0 +1,184 @@
import unittest
from toolz import curry
from . import test_helpers as th
from .split_paths import (
split_paths
)
from .lib import (
directed_graph,
pick_keys
)
if __name__ == "__main__":
unittest.main()
# Making sure vertex attrs are preserved.
vertex_props_dict = {
"Root1": {"a": 1, "b": 1},
"B": {"b": 2},
"X": {"x": 3}
}
def make_test_graph():
edges = [
("Root1", "A"),
("A", "B"),
("A", "D"),
("D", "E"),
("B", "D"),
("B", "F"),
("Root2", "B"),
("Root3", "C")
]
detached_vertices = ["X"]
vertex_props = vertex_props_dict.items()
return directed_graph(edges, detached_vertices, vertex_props)
class CustomAssertions:
@curry
def assertResultKeys(self, keys, result):
self.assertListEqual(
list(result.keys()),
keys
)
return result
class Test(
unittest.TestCase,
CustomAssertions,
th.CustomAssertions
):
def test_empty_paths(self):
input_graph = make_test_graph()
result = self.assertResultKeys(
["rest"],
split_paths([], input_graph)
)
self.assertGraphEqual(
result["rest"],
input_graph
)
def test_empty_graph(self):
empty_graph = directed_graph([])
def test_empty(paths):
result = self.assertResultKeys(
["rest"],
split_paths(paths, empty_graph)
)
self.assertGraphEqual(
result["rest"],
empty_graph
)
test_empty([])
test_empty(["B"])
def test_split_paths_single(self):
result = self.assertResultKeys(
["main", "common", "rest"],
split_paths(["B"], make_test_graph())
)
self.assertGraphEqual(
result["main"],
directed_graph(
[
("B", "F")
],
None,
pick_keys(["B"], vertex_props_dict).items()
)
)
self.assertGraphEqual(
result["rest"],
directed_graph(
[
("Root1", "A"),
("Root3", "C")
],
["Root2", "X"],
pick_keys(["Root1", "X"], vertex_props_dict).items()
)
)
self.assertGraphEqual(
result["common"],
directed_graph([("D", "E")])
)
def test_split_paths_multi(self):
result = self.assertResultKeys(
["main", "common", "rest"],
split_paths(["B", "Root3"], make_test_graph())
)
self.assertGraphEqual(
result["main"],
directed_graph(
[
("B", "F"),
("Root3", "C")
],
None,
pick_keys(["B"], vertex_props_dict).items()
)
)
self.assertGraphEqual(
result["rest"],
directed_graph(
[("Root1", "A")],
["Root2", "X"],
pick_keys(["Root1", "X"], vertex_props_dict).items()
)
)
self.assertGraphEqual(
result["common"],
directed_graph([("D", "E")])
)
def test_split_no_common(self):
result = self.assertResultKeys(
["main", "rest"],
split_paths(["D"], make_test_graph())
)
self.assertGraphEqual(
result["main"],
directed_graph([("D", "E")])
)
self.assertGraphEqual(
result["rest"],
directed_graph(
[
("Root1", "A"),
("A", "B"),
("B", "F"),
("Root2", "B"),
("Root3", "C"),
],
["X"],
pick_keys(["Root1", "B", "X"], vertex_props_dict).items()
)
)
@@ -0,0 +1,67 @@
from toolz import curry
from toolz import curried as tlz
from operator import attrgetter
from .lib import (
debug,
debug_plot,
DEBUG_PLOT,
find_vertex_by_name_or_none,
is_None,
subcomponent_multi
)
call_counts = {
"in": 0,
"out": 0
}
@curry
def subcomponent(mode, paths, graph):
if DEBUG_PLOT:
global call_counts
graph_name_prefix = f"subcomponent_{mode}_{call_counts[mode]}_"
call_counts[mode] += 1
layout = graph.layout('tree')
debug_plot(graph, f"{graph_name_prefix}input", layout=layout)
path_indices = tlz.compose(
tlz.map(attrgetter('index')),
tlz.remove(is_None),
tlz.map(find_vertex_by_name_or_none(graph))
)(paths)
debug("path_indices", path_indices)
main_indices = list(subcomponent_multi(graph, path_indices, mode))
debug('main_indices', main_indices)
if DEBUG_PLOT:
def choose_color(index):
if (index in main_indices):
return "green"
else:
return "red"
vertex_color = [choose_color(v.index) for v in graph.vs]
debug_plot(
graph,
f"{graph_name_prefix}result",
layout=layout,
vertex_color=vertex_color
)
return {
"main": graph.induced_subgraph(main_indices),
"rest": graph - main_indices
}
subcomponent_in = subcomponent("in")
subcomponent_out = subcomponent("out")
@@ -0,0 +1,219 @@
import unittest
from . import test_helpers as th
from .subcomponent import (
subcomponent_out,
subcomponent_in
)
from .lib import (
pick_keys,
directed_graph,
empty_directed_graph
)
if __name__ == "__main__":
unittest.main()
# Making sure vertex attrs are preserved.
vertex_props_dict = {
"Root1": {"a": 1, "b": 1},
"B": {"b": 2},
"X": {"x": 3}
}
def make_test_graph():
edges = [
("Root1", "A"),
("A", "B"),
("A", "C"),
("B", "D"),
("B", "E"),
("Root2", "B"),
("Root3", "C"),
]
detached_vertices = ["X"]
vertex_props = vertex_props_dict.items()
return directed_graph(edges, detached_vertices, vertex_props)
class CustomAssertions:
def assertResultKeys(self, result):
self.assertListEqual(
list(result.keys()),
["main", "rest"]
)
return result
class Test(
unittest.TestCase,
CustomAssertions,
th.CustomAssertions
):
def test_empty_paths(self):
def test(func):
input_graph = make_test_graph()
result = self.assertResultKeys(
func([], input_graph)
)
self.assertGraphEqual(
result["main"],
empty_directed_graph()
)
self.assertGraphEqual(
result["rest"],
input_graph
)
test(subcomponent_out)
test(subcomponent_in)
def test_empty_graph(self):
def test(func):
empty_graph = empty_directed_graph()
def test_empty(paths):
result = self.assertResultKeys(
func(paths, empty_graph)
)
self.assertGraphEqual(
result["main"],
empty_graph
)
self.assertGraphEqual(
result["rest"],
empty_graph
)
test_empty([])
test_empty(["B"])
test(subcomponent_out)
test(subcomponent_in)
def test_subcomponent_out(self):
result = self.assertResultKeys(
subcomponent_out(["B"], make_test_graph())
)
self.assertGraphEqual(
result["main"],
directed_graph(
[
("B", "D"),
("B", "E")
],
None,
pick_keys(["B"], vertex_props_dict).items()
)
)
self.assertGraphEqual(
result["rest"],
directed_graph(
[
("Root1", "A"),
("A", "C"),
("Root3", "C")
],
["Root2", "X"],
pick_keys(["Root1", "X"], vertex_props_dict).items()
)
)
def test_subcomponent_out_multi(self):
result = self.assertResultKeys(
subcomponent_out(["B", "Root3"], make_test_graph())
)
self.assertGraphEqual(
result["main"],
directed_graph(
[
("B", "D"),
("B", "E"),
("Root3", "C")
],
None,
pick_keys(["B"], vertex_props_dict).items()
)
)
self.assertGraphEqual(
result["rest"],
directed_graph(
[("Root1", "A")],
["Root2", "X"],
pick_keys(["Root1", "X"], vertex_props_dict).items()
)
)
def test_subcomponent_in(self):
result = self.assertResultKeys(
subcomponent_in(["B"], make_test_graph())
)
self.assertGraphEqual(
result["main"],
directed_graph(
[
("Root1", "A"),
("A", "B"),
("Root2", "B")
],
None,
pick_keys(["Root1", "B"], vertex_props_dict).items()
)
)
self.assertGraphEqual(
result["rest"],
directed_graph(
[("Root3", "C")],
["D", "E", "X"],
pick_keys(["X"], vertex_props_dict).items()
)
)
def test_subcomponent_in_multi(self):
result = self.assertResultKeys(
subcomponent_in(["B", "Root3"], make_test_graph())
)
self.assertGraphEqual(
result["main"],
directed_graph(
[
("Root1", "A"),
("A", "B"),
("Root2", "B"),
],
["Root3"],
pick_keys(["Root1", "B"], vertex_props_dict).items()
)
)
self.assertGraphEqual(
result["rest"],
directed_graph(
[],
["C", "D", "E", "X"],
pick_keys(["X"], vertex_props_dict).items()
)
)
@@ -0,0 +1,37 @@
from toolz import curried as tlz
from .lib import (
not_None,
graph_vertex_index_to_name
)
def edges_as_set(graph):
return frozenset(
(
graph_vertex_index_to_name(graph, e.source),
graph_vertex_index_to_name(graph, e.target)
) for e in graph.es
)
class CustomAssertions:
def assertGraphEqual(self, g1, g2):
self.assertSetEqual(
frozenset(g1.vs["name"]),
frozenset(g2.vs["name"])
)
self.assertSetEqual(
edges_as_set(g1),
edges_as_set(g2)
)
for name in g1.vs["name"]:
def get_vertex_attrs(g):
return tlz.valfilter(not_None, g.vs.find(name).attributes())
self.assertDictEqual(
get_vertex_attrs(g1),
get_vertex_attrs(g2),
)
@@ -0,0 +1,17 @@
from setuptools import setup
setup(
name="flatten_references_graph",
version="0.1.0",
author="Adrian Gierakowski",
packages=["flatten_references_graph"],
install_requires=[
"igraph",
"toolz"
],
entry_points={
"console_scripts": [
"flatten_references_graph=flatten_references_graph.__main__:main"
]
}
)
+5 -5
View File
@@ -6,23 +6,23 @@
rustPlatform.buildRustPackage rec {
pname = "git-graph";
version = "unstable-2023-01-14";
version = "0.6.0";
src = fetchFromGitHub {
owner = "mlange-42";
repo = pname;
rev = "9bd54eb0aed6f108364bce9ad0bdff12077038fc";
hash = "sha256-tMM/mpt9yzZYSpnOGBuGLM0XTJiiyChfUrERMuyn3mQ=";
rev = "refs/tags/${version}";
hash = "sha256-xYUpLujePO1MS0c25UJX5rRdmPzkaFgF5zJonzQOJqM=";
};
cargoHash = "sha256-ZLF/l2HnIbmkayWXhjYr01M6lGaGiK2UYyp654ncxgo=";
cargoHash = "sha256-y5tVjWj/LczblkL793878vzDG0Gtj3kIo2jZlRA6GJE=";
meta = with lib; {
description = "Command line tool to show clear git graphs arranged for your branching model";
homepage = "https://github.com/mlange-42/git-graph";
license = licenses.mit;
broken = stdenv.hostPlatform.isDarwin;
maintainers = with maintainers; [ cafkafk ];
maintainers = with maintainers; [ cafkafk matthiasbeyer ];
mainProgram = "git-graph";
};
}
-87
View File
@@ -1,87 +0,0 @@
{ python3Packages
, lib
, fetchFromGitLab
, meson
, pkg-config
, glib
, ninja
, desktop-file-utils
, gobject-introspection
, gtk3
, libnotify
, dbus
, wrapGAppsHook3
}:
python3Packages.buildPythonApplication rec {
pname = "gkraken";
version = "1.2.0";
src = fetchFromGitLab {
owner = "leinardi";
repo = "gkraken";
rev = version;
sha256 = "0hxlh0319rl28iba02917z3n6d5cq2qcgpj2ng31bkjjhlvvfm2g";
};
format = "other";
postPatch = ''
patchShebangs scripts/meson_post_install.py
'';
nativeBuildInputs = [
meson
pkg-config
gobject-introspection
glib
ninja
gtk3
desktop-file-utils
wrapGAppsHook3
];
buildInputs = [
glib
gtk3
libnotify
dbus
];
propagatedBuildInputs = with python3Packages; [
pygobject3
peewee
rx
injector
liquidctl
pyxdg
requests
matplotlib
dbus-python
];
dontWrapGApps = true;
# Extract udev rules from python code
postInstall = ''
mkdir -p $out/lib/udev/rules.d
sed -e '/\s*\(from\|@singleton\|@inject\)/d' $src/gkraken/interactor/udev_interactor.py > udev_interactor.py
python -c 'from udev_interactor import _UDEV_RULE; print(_UDEV_RULE)' > $out/lib/udev/rules.d/60-gkraken.rules
'';
preFixup = ''
makeWrapperArgs+=(
"''${gappsWrapperArgs[@]}"
)
'';
meta = with lib; {
description = "GUI that allows to control the cooling (fan and/or pump profiles) of NZXT Kraken AIO liquid coolers from Linux";
homepage = "https://gitlab.com/leinardi/gkraken";
changelog = "https://gitlab.com/leinardi/gkraken/-/tags/${version}";
license = licenses.gpl3Plus;
maintainers = with maintainers; [ OPNA2608 ];
platforms = platforms.linux;
mainProgram = "gkraken";
};
}
+80
View File
@@ -0,0 +1,80 @@
{
buildGoModule,
buildNpmPackage,
nix-update-script,
versionCheckHook,
fetchFromGitHub,
lib,
}:
let
version = "0.8.0";
rev = "a1e92b4bcdaf81d5b5e8f3edab60e33549e9009b";
src = fetchFromGitHub {
repo = "gose";
owner = "stv0g";
inherit rev;
hash = "sha256-Vcp9gMJ296KS8OFOIcWvvYrS+LrOVlN3KN20yJxoo5U=";
};
frontend = buildNpmPackage {
pname = "gose-frontend";
inherit version;
src = "${src}/frontend";
npmDepsHash = "sha256-p24s2SgCL8E9vUoZEyWSrd15IdkprneAXS7dwb7UbyA=";
installPhase = ''
runHook preInstall
find ./dist
mkdir $out
cp -r dist/* $out/
runHook postInstall
'';
};
in
buildGoModule {
pname = "gose";
inherit version;
inherit src;
vendorHash = "sha256-U/umJ6McCuD0HARVMj1JXHOpVxcph16z7Y7i47Nf3cg=";
CGO_ENABLED = 0;
postInstall = ''
mv $out/bin/cmd $out/bin/gose
'';
tags = [ "embed" ];
ldflags = [
"-s"
"-w"
"-X main.version=${version}"
"-X main.commit=${rev}"
"-X main.builtBy=Nix"
];
# Skipping test which relies on internet services.
checkFlags = "-skip TestShortener";
nativeInstallCheckInputs = [
versionCheckHook
];
versionCheckProgramArg = "-version";
doInstallCheck = true;
prePatch = ''
cp -r ${frontend} frontend/dist
'';
passthru.updateScript = nix-update-script { };
meta = {
description = "Modern and scalable file-uploader focusing on scalability and simplicity";
homepage = "https://github.com/stv0g/gose";
license = lib.licenses.asl20;
maintainers = with lib.maintainers; [ stv0g ];
mainProgram = "gose";
};
}
+3 -3
View File
@@ -27,13 +27,13 @@ let
in
stdenv.mkDerivation (finalAttrs: {
pname = "melonDS";
version = "0.9.5-unstable-2024-09-29";
version = "1.0rc-unstable-2024-11-27";
src = fetchFromGitHub {
owner = "melonDS-emu";
repo = "melonDS";
rev = "e9446fa9dc3ba64c22f8e5bb611ad394b5729eda";
hash = "sha256-0YNF9NPKaaKzC3d8cXG4Od3AUc1zxVtuB5u2Q8ruwr0=";
rev = "cba838dd52a8d163c59e312d75f451a04cea9b48";
hash = "sha256-0Pv2ARbQkYaGDQ7AMLlMdK1MjnsctPIIWxGz2XNTNmI=";
};
nativeBuildInputs = [
+4
View File
@@ -29,6 +29,10 @@ python3Packages.buildPythonApplication rec {
pyqt5
];
postFixup = ''
mv $out/${python3Packages.python.sitePackages}/usr/share $out/share
'';
pythonImportsCheck = [ "meteo_qt" ];
makeWrapperArgs = [ "\${qtWrapperArgs[@]}" ];
File diff suppressed because it is too large Load Diff
+2 -7
View File
@@ -92,13 +92,8 @@ rustPlatform.buildRustPackage rec {
hash = "sha256-aflzpJZ48AFBqNTssZl2KxkspQb662nGkEU6COIluxk=";
};
cargoLock = {
lockFile = ./Cargo.lock;
outputHashes = {
"bindgen_cuda-0.1.6" = "sha256-OWGcQxT+x5HyIFskNVWpPr6Qfkh6Mv/g4PVSm5oA27g=";
"candle-core-0.7.2" = "sha256-OovBzD1gEYToa3HT8oQtbY6sDy0heRwAH2cK7gz5Jm0=";
};
};
useFetchCargoVendor = true;
cargoHash = "sha256-USp8siEXVjtkPoCHfQjDYPtgLfNHcy02LSUNdwDbxgs=";
nativeBuildInputs = [
pkg-config
+1 -1
View File
@@ -17,7 +17,7 @@ buildGoModule rec {
};
modRoot = "./cli";
tags = "extension";
tags = [ "extension" ];
ldflags = [
"-s"
"-w"
+3 -3
View File
@@ -5,16 +5,16 @@
buildGoModule rec {
pname = "ooniprobe-cli";
version = "3.23.0";
version = "3.24.0";
src = fetchFromGitHub {
owner = "ooni";
repo = "probe-cli";
rev = "v${version}";
hash = "sha256-ey71XA/ZtN6AI/DyjqFGcY5XW9H0RoOjrmjDEIgJMkM=";
hash = "sha256-jg09yIX3jHMp0PwSwy2RMwYhZm0haJFTmpwcyTc5I0A=";
};
vendorHash = "sha256-wuCqyr7JuzRHJE8ct1DRi9aFJWeHdF1sxUjvQiBMa6I=";
vendorHash = "sha256-QH5PyCvfJWWDhjH6Jo8O3wBq3MwfpHWKSuYMmpaqjWk=";
subPackages = [ "cmd/ooniprobe" ];
+2 -2
View File
@@ -16,13 +16,13 @@
stdenv.mkDerivation rec {
pname = "pappl";
version = "1.4.7";
version = "1.4.8";
src = fetchFromGitHub {
owner = "michaelrsweet";
repo = "pappl";
rev = "refs/tags/v${version}";
hash = "sha256-Npry3H+QbAH19hoqAZuOwjpZwCPhOLewD8uKZlo4gdQ=";
hash = "sha256-vp0DQvsQzuiXb5QAXVEcXNvyDJeRM99aO9/baZw9QwA=";
};
outputs = [
+51
View File
@@ -0,0 +1,51 @@
{
lib,
fetchFromGitHub,
rustPlatform,
versionCheckHook,
openssl,
pkg-config,
}:
rustPlatform.buildRustPackage rec {
pname = "rattler-build";
version = "0.29.0";
src = fetchFromGitHub {
owner = "prefix-dev";
repo = "rattler-build";
rev = "refs/tags/v${version}";
hash = "sha256-MTCtDE0oX9vUIwO62sKkCGleooHU28Lq8eXzwiNPDMo=";
};
useFetchCargoVendor = true;
cargoHash = "sha256-QONzLC1LvKkbslXZQTqxb6RHgsoUX21MXXyLVtTSvxc=";
doCheck = false; # test requires network access
nativeBuildInputs = [
pkg-config
];
buildInputs = [
openssl
];
doInstallCheck = true;
nativeInstallCheckInputs = [
versionCheckHook
];
versionCheckProgramArg = [ "-V" ];
meta = {
description = "Universal package builder for Windows, macOS and Linux";
homepage = "https://github.com/prefix-dev/rattler-build";
changelog = "https://github.com/prefix-dev/rattler-build/releases/tag/v${version}";
license = lib.licenses.bsd3;
maintainers = with lib.maintainers; [ genga898 ];
mainProgram = "rattler-build";
};
}
+3 -3
View File
@@ -8,17 +8,17 @@
rustPlatform.buildRustPackage rec {
pname = "typstyle";
version = "0.12.4";
version = "0.12.5";
src = fetchFromGitHub {
owner = "Enter-tainer";
repo = "typstyle";
rev = "refs/tags/v${version}";
hash = "sha256-PMx7vo0HPknrZUQooehuFYRTfiWSpLYw03zcfVuNNEc=";
hash = "sha256-28+B7lov2sUBDZUGHKBL1XzOM9q43idllQ0CPlZEdOw=";
};
useFetchCargoVendor = true;
cargoHash = "sha256-ughfZ9+oR7vjlBW7jl0w99FM8eEUYODh6jJ0no8tNvI=";
cargoHash = "sha256-rSPXfGZXwrnCeG9RgjQdKQkacI4phzRF3AwfKu17Yvc=";
# Disabling tests requiring network access
checkFlags = [
+2 -2
View File
@@ -28,7 +28,7 @@ stdenv.mkDerivation rec {
outputs = [ "out" "dev" ];
src = fetchurl {
url = "https://github.com/AbiWord/${pname}/releases/download/v${version}/${pname}-${version}.tar.gz";
url = "https://github.com/rrthomas/${pname}/releases/download/v${version}/${pname}-${version}.tar.gz";
hash = "sha256-2aWhDcmzikOzoPoix27W67fgnrU1r/YpVK/NvUDv/2s=";
};
@@ -75,7 +75,7 @@ stdenv.mkDerivation rec {
meta = with lib; {
description = "Generic spell checking library";
homepage = "https://abiword.github.io/enchant/";
homepage = "https://rrthomas.github.io/enchant/";
license = licenses.lgpl21Plus; # with extra provision for non-free checkers
maintainers = with maintainers; [ jtojnar ];
platforms = platforms.unix;
+1
View File
@@ -462,6 +462,7 @@ mapAliases {
topGit = top-git;
}; # Added 2021-01-14
gkraken = throw "'gkraken' has been deprecated by upstream. Consider using the replacement 'coolercontrol' instead."; # Added 2024-11-22
glew-egl = lib.warn "'glew-egl' is now provided by 'glew' directly" glew; # Added 2024-08-11
glfw-wayland = glfw; # Added 2024-04-19
glfw-wayland-minecraft = glfw3-minecraft; # Added 2024-05-08
+2
View File
@@ -779,6 +779,8 @@ with pkgs;
referencesByPopularity = callPackage ../build-support/references-by-popularity { };
dockerMakeLayers = callPackage ../build-support/docker/make-layers.nix { };
removeReferencesTo = callPackage ../build-support/remove-references-to {
inherit (darwin) signingUtils;
};