about summary refs log tree commit diff
path: root/nixpkgs/maintainers
diff options
context:
space:
mode:
authorAlyssa Ross <hi@alyssa.is>2024-03-01 11:40:12 +0100
committerAlyssa Ross <hi@alyssa.is>2024-03-01 11:40:12 +0100
commitbf6d657e5dbcb5e39fda280ef7e86b2a7794ca86 (patch)
tree8eb035cbab19794f6415cc460fac7226f7a58afc /nixpkgs/maintainers
parent66f707d69f1e423db5a35c2fe43b32781125a9af (diff)
parent09c1497ce5d4ed4a0edfdd44450d3048074cb300 (diff)
downloadnixlib-bf6d657e5dbcb5e39fda280ef7e86b2a7794ca86.tar
nixlib-bf6d657e5dbcb5e39fda280ef7e86b2a7794ca86.tar.gz
nixlib-bf6d657e5dbcb5e39fda280ef7e86b2a7794ca86.tar.bz2
nixlib-bf6d657e5dbcb5e39fda280ef7e86b2a7794ca86.tar.lz
nixlib-bf6d657e5dbcb5e39fda280ef7e86b2a7794ca86.tar.xz
nixlib-bf6d657e5dbcb5e39fda280ef7e86b2a7794ca86.tar.zst
nixlib-bf6d657e5dbcb5e39fda280ef7e86b2a7794ca86.zip
Merge branch 'nixos-unstable-small' of https://github.com/NixOS/nixpkgs
Diffstat (limited to 'nixpkgs/maintainers')
-rw-r--r--nixpkgs/maintainers/maintainer-list.nix61
-rw-r--r--nixpkgs/maintainers/scripts/bootstrap-files/README.md2
-rwxr-xr-xnixpkgs/maintainers/scripts/kde/collect-licenses.sh31
-rwxr-xr-xnixpkgs/maintainers/scripts/kde/collect-logs.nu11
-rwxr-xr-xnixpkgs/maintainers/scripts/kde/collect-metadata.py36
-rwxr-xr-xnixpkgs/maintainers/scripts/kde/collect-missing-deps.py127
-rwxr-xr-xnixpkgs/maintainers/scripts/kde/generate-sources.py113
-rw-r--r--nixpkgs/maintainers/scripts/kde/utils.py185
8 files changed, 565 insertions, 1 deletions
diff --git a/nixpkgs/maintainers/maintainer-list.nix b/nixpkgs/maintainers/maintainer-list.nix
index e89b81ec0e96..e25af81c96a8 100644
--- a/nixpkgs/maintainers/maintainer-list.nix
+++ b/nixpkgs/maintainers/maintainer-list.nix
@@ -203,6 +203,15 @@
       fingerprint = "D292 365E 3C46 A5AA 75EE  B30B 78DB 7EDE 3540 794B";
     }];
   };
+  _6543 = {
+    email = "6543@obermui.de";
+    github = "6543";
+    githubId = 24977596;
+    name = "6543";
+    keys = [{
+      fingerprint = "8722 B61D 7234 1082 553B  201C B8BE 6D61 0E61 C862";
+    }];
+  };
   _6AA4FD = {
     email = "f6442954@gmail.com";
     github = "6AA4FD";
@@ -5805,6 +5814,13 @@
     githubId = 8706;
     name = "Rafael Fernández López";
   };
+  erethon = {
+    email = "dgrig@erethon.com";
+    matrix = "@dgrig:erethon.com";
+    github = "erethon";
+    githubId = 1254842;
+    name = "Dionysis Grigoropoulos";
+  };
   ericbmerritt = {
     email = "eric@afiniate.com";
     github = "ericbmerritt";
@@ -7031,6 +7047,15 @@
     github = "ghostbuster91";
     githubId = 5662622;
   };
+  ghthor = {
+    email = "ghthor@gmail.com";
+    github = "ghthor";
+    githubId = 160298;
+    name = "Will Owens";
+    keys = [{
+      fingerprint = "8E98 BB01 BFF8 AEA4 E303  FC4C 8074 09C9 2CE2 3033";
+    }];
+  };
   ghuntley = {
     email = "ghuntley@ghuntley.com";
     github = "ghuntley";
@@ -7606,6 +7631,12 @@
     githubId = 287769;
     name = "Sergii Paryzhskyi";
   };
+  heijligen = {
+    email = "src@posteo.de";
+    github = "heijligen";
+    githubId = 19170376;
+    name = "Thomas Heijligen";
+  };
   heisfer = {
     email = "heisfer@refract.dev";
     github = "heisfer";
@@ -11023,6 +11054,12 @@
     githubId = 591860;
     name = "Lionello Lunesu";
   };
+  litchipi = {
+    email = "litchi.pi@proton.me";
+    github = "litchipi";
+    githubId = 61109829;
+    name = "Litchi Pi";
+  };
   livnev = {
     email = "lev@liv.nev.org.uk";
     github = "livnev";
@@ -16674,6 +16711,12 @@
     githubId = 6445619;
     name = "Ruben Cano Diaz";
   };
+  RudiOnTheAir = {
+    name = "Rüdiger Schwoon";
+    email = "wolf@schwoon.info";
+    github = "RudiOnTheAir";
+    githubId = 47517341;
+  };
   rudolfvesely = {
     name = "Rudolf Vesely";
     email = "i@rudolfvesely.com";
@@ -17162,6 +17205,12 @@
       fingerprint = "E173 237A C782 296D 98F5  ADAC E13D FD4B 4712 7951";
     }];
   };
+  sdht0 = {
+    email = "nixpkgs@sdht.in";
+    github = "sdht0";
+    githubId = 867424;
+    name = "Siddhartha Sahu";
+  };
   sdier = {
     email = "scott@dier.name";
     matrix = "@sdier:matrix.org";
@@ -17255,6 +17304,12 @@
     githubId = 1286668;
     name = "Thilo Uttendorfer";
   };
+  sentientmonkey = {
+    email = "swindsor@gmail.com";
+    github = "sentientmonkey";
+    githubId = 9032;
+    name = "Scott Windsor";
+  };
   sents = {
     email = "finn@krein.moe";
     github = "sents";
@@ -20243,6 +20298,12 @@
     githubId = 326263;
     name = "Danny Wilson";
   };
+  vizid = {
+    email = "vizid1337@gmail.com";
+    github = "ViZiD";
+    githubId = 7444430;
+    name = "Radik Islamov";
+  };
   vklquevs = {
     email = "vklquevs@gmail.com";
     github = "vklquevs";
diff --git a/nixpkgs/maintainers/scripts/bootstrap-files/README.md b/nixpkgs/maintainers/scripts/bootstrap-files/README.md
index ae385cbd6ce8..b55878f34192 100644
--- a/nixpkgs/maintainers/scripts/bootstrap-files/README.md
+++ b/nixpkgs/maintainers/scripts/bootstrap-files/README.md
@@ -39,7 +39,7 @@ target:
    ```
 
    To validate cross-targets `binfmt` `NixOS` helper can be useful.
-   For `riscv64-unknown-linux-gnu` the `/etc/nixox/configuraqtion.nix`
+   For `riscv64-unknown-linux-gnu` the `/etc/nixos/configuration.nix`
    entry would be `boot.binfmt.emulatedSystems = [ "riscv64-linux" ]`.
 
 3. Propose the commit as a PR to update bootstrap tarballs, tag people
diff --git a/nixpkgs/maintainers/scripts/kde/collect-licenses.sh b/nixpkgs/maintainers/scripts/kde/collect-licenses.sh
new file mode 100755
index 000000000000..87da901c255c
--- /dev/null
+++ b/nixpkgs/maintainers/scripts/kde/collect-licenses.sh
@@ -0,0 +1,31 @@
+#!/usr/bin/env nix-shell
+#!nix-shell -i bash -p gnutar jq reuse
+set -eu
+cd "$(dirname "$(readlink -f "$0")")"/../../..
+
+TMPDIR=$(mktemp -d)
+trap 'rm -rf $TMPDIR' EXIT
+
+echo "# Prebuilding sources..."
+nix-build -A kdePackages.sources --no-link || true
+
+echo "# Evaluating sources..."
+declare -A sources
+eval "$(nix-instantiate --eval -A kdePackages.sources --json --strict | jq 'to_entries[] | "sources[" + .key + "]=" + .value' -r)"
+
+echo "# Collecting licenses..."
+for k in "${!sources[@]}"; do
+    echo "- Processing $k..."
+
+    if [ ! -f "${sources[$k]}" ]; then
+        echo "Not found!"
+        continue
+    fi
+
+    mkdir "$TMPDIR/$k"
+    tar -C "$TMPDIR/$k" -xf "${sources[$k]}"
+
+    (cd "$TMPDIR/$k"; reuse lint --json) | jq --arg name "$k" '{$name: .summary.used_licenses | sort}' -c > "$TMPDIR/$k.json"
+done
+
+jq -s 'add' -S "$TMPDIR"/*.json > pkgs/kde/generated/licenses.json
diff --git a/nixpkgs/maintainers/scripts/kde/collect-logs.nu b/nixpkgs/maintainers/scripts/kde/collect-logs.nu
new file mode 100755
index 000000000000..1d07fa9d2caf
--- /dev/null
+++ b/nixpkgs/maintainers/scripts/kde/collect-logs.nu
@@ -0,0 +1,11 @@
+#!/usr/bin/env nix-shell
+#!nix-shell -i nu -p nushell
+cd $"($env.FILE_PWD)/../../.."
+
+mkdir logs
+nix-env -qaP -f . -A kdePackages --json --out-path | from json | values | par-each { |it|
+    echo $"Processing ($it.pname)..."
+    if "outputs" in $it {
+        nix-store --read-log $it.outputs.out | save -f $"logs/($it.pname).log"
+    }
+}
diff --git a/nixpkgs/maintainers/scripts/kde/collect-metadata.py b/nixpkgs/maintainers/scripts/kde/collect-metadata.py
new file mode 100755
index 000000000000..eaa619647136
--- /dev/null
+++ b/nixpkgs/maintainers/scripts/kde/collect-metadata.py
@@ -0,0 +1,36 @@
+#!/usr/bin/env nix-shell
+#!nix-shell -i python3 -p "python3.withPackages(ps: [ ps.click ps.pyyaml ])"
+import pathlib
+
+import click
+
+import utils
+
+@click.command
+@click.argument(
+    "repo-metadata",
+    type=click.Path(
+        exists=True,
+        file_okay=False,
+        resolve_path=True,
+        path_type=pathlib.Path,
+    ),
+)
+@click.option(
+    "--nixpkgs",
+    type=click.Path(
+        exists=True,
+        file_okay=False,
+        resolve_path=True,
+        writable=True,
+        path_type=pathlib.Path,
+    ),
+    default=pathlib.Path(__file__).parent.parent.parent.parent
+)
+def main(repo_metadata: pathlib.Path, nixpkgs: pathlib.Path):
+    metadata = utils.KDERepoMetadata.from_repo_metadata_checkout(repo_metadata)
+    out_dir = nixpkgs / "pkgs/kde/generated"
+    metadata.write_json(out_dir)
+
+if __name__ == "__main__":
+    main()  # type: ignore
diff --git a/nixpkgs/maintainers/scripts/kde/collect-missing-deps.py b/nixpkgs/maintainers/scripts/kde/collect-missing-deps.py
new file mode 100755
index 000000000000..f3943338b57f
--- /dev/null
+++ b/nixpkgs/maintainers/scripts/kde/collect-missing-deps.py
@@ -0,0 +1,127 @@
+#!/usr/bin/env nix-shell
+#!nix-shell -i python3 -p python3
+import pathlib
+
+OK_MISSING = {
+    # we don't use precompiled QML
+    'Qt6QuickCompiler',
+    'Qt6QmlCompilerPlusPrivate',
+    # usually used for version numbers
+    'Git',
+    # useless by itself, will warn if something else is not found
+    'PkgConfig',
+    # license verification
+    'ReuseTool',
+    # dev only
+    'ClangFormat',
+    # doesn't exist
+    'Qt6X11Extras',
+}
+
+OK_MISSING_BY_PACKAGE = {
+    "angelfish": {
+        "Qt6Feedback",  # we don't have it
+    },
+    "attica": {
+        "Python3",  # only used for license checks
+    },
+    "discover": {
+        "rpm-ostree-1",  # we don't have rpm-ostree (duh)
+        "Snapd",  # we don't have snaps and probably never will
+    },
+    "elisa": {
+        "UPNPQT",  # upstream says it's broken
+    },
+    "extra-cmake-modules": {
+        "Sphinx",  # only used for docs, bloats closure size
+        "QCollectionGenerator"
+    },
+    "kio-extras-kf5": {
+        "KDSoapWSDiscoveryClient",  # actually vendored on KF5 version
+    },
+    "kitinerary": {
+        "OsmTools",  # used for map data updates, we use prebuilt
+    },
+    "kosmindoormap": {
+        "OsmTools",  # same
+        "Protobuf",
+    },
+    "kpty": {
+        "UTEMPTER",  # we don't have it and it probably wouldn't work anyway
+    },
+    "kpublictransport": {
+        "OsmTools",  # same
+        "PolyClipping",
+        "Protobuf",
+    },
+    "krfb": {
+        "Qt6XkbCommonSupport",  # not real
+    },
+    "kuserfeedback": {
+        "Qt6Svg",  # all used for backend console stuff we don't ship
+        "QmlLint",
+        "Qt6Charts",
+        "FLEX",
+        "BISON",
+        "Php",
+        "PhpUnit",
+    },
+    "kwin": {
+        "display-info",  # newer versions identify as libdisplay-info
+    },
+    "mlt": {
+        "Qt5",  # intentionally disabled
+        "SWIG",
+    },
+    "plasma-desktop": {
+        "scim",  # upstream is dead, not packaged in Nixpkgs
+    },
+    "powerdevil": {
+        "DDCUtil",  # cursed, intentionally disabled
+    },
+    "pulseaudio-qt": {
+        "Qt6Qml",  # tests only
+        "Qt6Quick",
+    },
+    "syntax-highlighting": {
+        "XercesC",  # only used for extra validation at build time
+    }
+}
+
+def main():
+    here = pathlib.Path(__file__).parent.parent.parent.parent
+    logs = (here / "logs").glob("*.log")
+
+    for log in sorted(logs):
+        pname = log.stem
+
+        missing = []
+        is_in_block = False
+        with log.open(errors="replace") as fd:
+            for line in fd:
+                line = line.strip()
+                if line.startswith("--   No package '"):
+                    package = line.removeprefix("--   No package '").removesuffix("' found")
+                    missing.append(package)
+                if line == "-- The following OPTIONAL packages have not been found:" or line == "-- The following RECOMMENDED packages have not been found:":
+                    is_in_block = True
+                elif line.startswith("--") and is_in_block:
+                    is_in_block = False
+                elif line.startswith("*") and is_in_block:
+                    package = line.removeprefix("* ")
+                    missing.append(package)
+
+        missing = {
+            package
+            for package in missing
+            if not any(package.startswith(i) for i in OK_MISSING | OK_MISSING_BY_PACKAGE.get(pname, set()))
+        }
+
+        if missing:
+            print(pname + ":")
+            for line in missing:
+                print("  -", line)
+            print()
+
+if __name__ == '__main__':
+    main()
diff --git a/nixpkgs/maintainers/scripts/kde/generate-sources.py b/nixpkgs/maintainers/scripts/kde/generate-sources.py
new file mode 100755
index 000000000000..e9f8c41ef4d7
--- /dev/null
+++ b/nixpkgs/maintainers/scripts/kde/generate-sources.py
@@ -0,0 +1,113 @@
+#!/usr/bin/env nix-shell
+#!nix-shell -i python3 -p "python3.withPackages(ps: [ ps.beautifulsoup4 ps.click ps.httpx ps.jinja2 ps.pyyaml ])
+import base64
+import binascii
+import json
+import pathlib
+from urllib.parse import urlparse
+
+import bs4
+import click
+import httpx
+import jinja2
+
+import utils
+
+
+LEAF_TEMPLATE = jinja2.Template('''
+{mkKdeDerivation}:
+mkKdeDerivation {
+  pname = "{{ pname }}";
+}
+'''.strip())
+
+ROOT_TEMPLATE = jinja2.Template('''
+{callPackage}: {
+  {%- for p in packages %}
+  {{ p }} = callPackage ./{{ p }} {};
+  {%- endfor %}
+}
+'''.strip());
+
+def to_sri(hash):
+    raw = binascii.unhexlify(hash)
+    b64 = base64.b64encode(raw).decode()
+    return f"sha256-{b64}"
+
+
+@click.command
+@click.argument(
+    "set",
+    type=click.Choice(["frameworks", "gear", "plasma"]),
+    required=True
+)
+@click.argument(
+    "version",
+    type=str,
+    required=True
+)
+@click.option(
+    "--nixpkgs",
+    type=click.Path(
+        exists=True,
+        file_okay=False,
+        resolve_path=True,
+        writable=True,
+        path_type=pathlib.Path,
+    ),
+    default=pathlib.Path(__file__).parent.parent.parent.parent
+)
+def main(set: str, version: str, nixpkgs: pathlib.Path):
+    root_dir = nixpkgs / "pkgs/kde"
+    set_dir = root_dir / set
+    generated_dir = root_dir / "generated"
+    metadata = utils.KDERepoMetadata.from_json(generated_dir)
+
+    set_url = {
+        "frameworks": "kf",
+        "gear": "releases",
+        "plasma": "plasma",
+    }[set]
+
+    sources = httpx.get(f"https://kde.org/info/sources/source-{set_url}-{version}.html")
+    sources.raise_for_status()
+    bs = bs4.BeautifulSoup(sources.text, features="html.parser")
+
+    results = {}
+    for item in bs.select("tr")[1:]:
+        link = item.select_one("td:nth-child(1) a")
+        assert link
+
+        hash = item.select_one("td:nth-child(3) tt")
+        assert hash
+
+        project_name, version = link.text.rsplit("-", maxsplit=1)
+        if project_name not in metadata.projects_by_name:
+            print(f"Warning: unknown tarball: {project_name}")
+
+        results[project_name] = {
+            "version": version,
+            "url": "mirror://kde" + urlparse(link.attrs["href"]).path,
+            "hash": to_sri(hash.text)
+        }
+
+        pkg_dir = set_dir / project_name
+        pkg_file = pkg_dir / "default.nix"
+        if not pkg_file.exists():
+            print(f"Generated new package: {set}/{project_name}")
+            pkg_dir.mkdir(parents=True, exist_ok=True)
+            with pkg_file.open("w") as fd:
+                fd.write(LEAF_TEMPLATE.render(pname=project_name) + "\n")
+
+    set_dir.mkdir(parents=True, exist_ok=True)
+    with (set_dir / "default.nix").open("w") as fd:
+        fd.write(ROOT_TEMPLATE.render(packages=results.keys()) + "\n")
+
+    sources_dir = generated_dir / "sources"
+    sources_dir.mkdir(parents=True, exist_ok=True)
+    with (sources_dir / f"{set}.json").open("w") as fd:
+        json.dump(results, fd, indent=2)
+
+
+if __name__ == "__main__":
+    main()  # type: ignore
diff --git a/nixpkgs/maintainers/scripts/kde/utils.py b/nixpkgs/maintainers/scripts/kde/utils.py
new file mode 100644
index 000000000000..7a82c4955c6b
--- /dev/null
+++ b/nixpkgs/maintainers/scripts/kde/utils.py
@@ -0,0 +1,185 @@
+import collections
+import dataclasses
+import functools
+import json
+import pathlib
+import subprocess
+
+import yaml
+
+class DataclassEncoder(json.JSONEncoder):
+    def default(self, it):
+        if dataclasses.is_dataclass(it):
+            return dataclasses.asdict(it)
+        return super().default(it)
+
+
+@dataclasses.dataclass
+class Project:
+    name: str
+    description: str | None
+    project_path: str
+    repo_path: str | None
+
+    def __hash__(self) -> int:
+        return hash(self.name)
+
+    @classmethod
+    def from_yaml(cls, path: pathlib.Path):
+        data = yaml.safe_load(path.open())
+        return cls(
+            name=data["identifier"],
+            description=data["description"],
+            project_path=data["projectpath"],
+            repo_path=data["repopath"]
+        )
+
+
+def get_git_commit(path: pathlib.Path):
+    return subprocess.check_output(["git", "-C", path, "rev-parse", "--short", "HEAD"]).decode().strip()
+
+
+def validate_unique(projects: list[Project], attr: str):
+    seen = set()
+    for item in projects:
+        attr_value = getattr(item, attr)
+        if attr_value in seen:
+            raise Exception(f"Duplicate {attr}: {attr_value}")
+        seen.add(attr_value)
+
+
+THIRD_PARTY = {
+    "third-party/appstream": "appstream-qt",
+    "third-party/cmark": "cmark",
+    "third-party/gpgme": "gpgme",
+    "third-party/kdsoap": "kdsoap",
+    "third-party/libaccounts-qt": "accounts-qt",
+    "third-party/libgpg-error": "libgpg-error",
+    "third-party/libquotient": "libquotient",
+    "third-party/packagekit-qt": "packagekit-qt",
+    "third-party/poppler": "poppler",
+    "third-party/qcoro": "qcoro",
+    "third-party/qmltermwidget": "qmltermwidget",
+    "third-party/qtkeychain": "qtkeychain",
+    "third-party/signond": "signond",
+    "third-party/taglib": "taglib",
+    "third-party/wayland-protocols": "wayland-protocols",
+    "third-party/wayland": "wayland",
+    "third-party/zxing-cpp": "zxing-cpp",
+}
+
+IGNORE = {
+    "kdesupport/phonon-directshow",
+    "kdesupport/phonon-mmf",
+    "kdesupport/phonon-mplayer",
+    "kdesupport/phonon-quicktime",
+    "kdesupport/phonon-waveout",
+    "kdesupport/phonon-xine"
+}
+
+WARNED = set()
+
+
+@dataclasses.dataclass
+class KDERepoMetadata:
+    version: str
+    projects: list[Project]
+    dep_graph: dict[Project, set[Project]]
+
+    @functools.cached_property
+    def projects_by_name(self):
+        return {p.name: p for p in self.projects}
+
+    @functools.cached_property
+    def projects_by_path(self):
+        return {p.project_path: p for p in self.projects}
+
+    def try_lookup_package(self, path):
+        if path in IGNORE:
+            return None
+        project = self.projects_by_path.get(path)
+        if project is None and path not in WARNED:
+            WARNED.add(path)
+            print(f"Warning: unknown project {path}")
+        return project
+
+    @classmethod
+    def from_repo_metadata_checkout(cls, repo_metadata: pathlib.Path):
+        projects = [
+            Project.from_yaml(metadata_file)
+            for metadata_file in repo_metadata.glob("projects-invent/**/metadata.yaml")
+        ] + [
+            Project(id, None, project_path, None)
+            for project_path, id in THIRD_PARTY.items()
+        ]
+
+        validate_unique(projects, "name")
+        validate_unique(projects, "project_path")
+
+        self = cls(
+            version=get_git_commit(repo_metadata),
+            projects=projects,
+            dep_graph={},
+        )
+
+        dep_specs = [
+            "dependency-data-common",
+            "dependency-data-kf6-qt6"
+        ]
+        dep_graph = collections.defaultdict(set)
+
+        for spec in dep_specs:
+            spec_path = repo_metadata / "dependencies" / spec
+            for line in spec_path.open():
+                line = line.strip()
+                if line.startswith("#"):
+                    continue
+                if not line:
+                    continue
+
+                dependent, dependency = line.split(": ")
+
+                dependent = self.try_lookup_package(dependent)
+                if dependent is None:
+                    continue
+
+                dependency = self.try_lookup_package(dependency)
+                if dependency is None:
+                    continue
+
+                dep_graph[dependent].add(dependency)
+
+        self.dep_graph = dep_graph
+
+        return self
+
+    def write_json(self, root: pathlib.Path):
+        root.mkdir(parents=True, exist_ok=True)
+
+        with (root / "projects.json").open("w") as fd:
+            json.dump(self.projects_by_name, fd, cls=DataclassEncoder, sort_keys=True, indent=2)
+
+        with (root / "dependencies.json").open("w") as fd:
+            deps = {k.name: sorted(dep.name for dep in v) for k, v in self.dep_graph.items()}
+            json.dump({"version": self.version, "dependencies": deps}, fd, cls=DataclassEncoder, sort_keys=True, indent=2)
+
+    @classmethod
+    def from_json(cls, root: pathlib.Path):
+        projects = [
+            Project(**v) for v in json.load((root / "projects.json").open()).values()
+        ]
+
+        deps = json.load((root / "dependencies.json").open())
+        self = cls(
+            version=deps["version"],
+            projects=projects,
+            dep_graph={},
+        )
+
+        dep_graph = collections.defaultdict(set)
+        for dependent, dependencies in deps["dependencies"].items():
+            for dependency in dependencies:
+                dep_graph[self.projects_by_name[dependent]].add(self.projects_by_name[dependency])
+
+        self.dep_graph = dep_graph
+        return self