├── modules ├── programs │ ├── git │ │ └── tests │ │ │ ├── enabled │ │ │ ├── .gitattributes │ │ │ ├── gitignore │ │ │ └── default.nix │ │ │ ├── default.nix │ │ │ └── settings │ │ │ ├── .gitattributes │ │ │ ├── gitignore │ │ │ ├── git │ │ │ └── config │ │ │ └── default.nix │ ├── treefmt.nix │ ├── project-manager.nix │ ├── just.nix │ ├── direnv.nix │ └── mercurial.nix ├── files │ └── tests │ │ ├── simple │ │ ├── meh │ │ └── default.nix │ │ └── default.nix ├── services │ ├── flakehub │ │ ├── tests │ │ │ ├── default.nix │ │ │ └── enabled │ │ │ │ ├── default.nix │ │ │ │ └── .github │ │ │ │ └── workflows │ │ │ │ └── flakehub-publish.yml │ │ └── default.nix │ ├── renovate │ │ ├── tests │ │ │ ├── default.nix │ │ │ └── simple │ │ │ │ ├── renovate.json │ │ │ │ └── default.nix │ │ └── default.nix │ ├── flakestry.nix │ ├── garnix.nix │ ├── github.nix │ └── nix-ci.nix ├── misc │ ├── editorconfig │ │ ├── tests │ │ │ ├── default.nix │ │ │ └── simple │ │ │ │ ├── editorconfig │ │ │ │ └── default.nix │ │ └── default.nix │ ├── version.nix │ ├── xdg.nix │ └── news.nix ├── lib │ ├── types │ │ ├── tests │ │ │ ├── default.nix │ │ │ ├── dag-merge-result.txt │ │ │ ├── dag-submodule.nix │ │ │ ├── dag-merge.nix │ │ │ └── gvariant-merge.nix │ │ ├── dag.nix │ │ └── default.nix │ ├── shell.nix │ ├── stdlib-extended.nix │ ├── default.nix │ ├── strings.nix │ ├── generators.nix │ ├── path.nix │ ├── dag.nix │ └── gvariant.nix ├── all-modules.nix ├── modules.nix ├── default.nix └── lib-bash │ └── activation-init.bash ├── docs ├── manual │ ├── 3rd-party │ │ └── collections.md │ ├── options.md │ ├── faq.md │ ├── faq │ │ ├── session-variables.md │ │ ├── ca-desrt-dconf.md │ │ ├── unstable.md │ │ ├── change-package-module.md │ │ └── collision.md │ ├── usage │ │ ├── updating.md │ │ ├── rollbacks.md │ │ ├── dotfiles.md │ │ └── configuration.md │ ├── introduction │ │ └── devShell.md │ ├── quick-start.md │ ├── manual.md │ ├── quick-start │ │ ├── new-project.md │ │ └── existing-project.md │ ├── 3rd-party.md │ ├── nix-flakes.md │ ├── preface.md │ ├── writing-modules.md │ ├── contributing.md │ ├── nix-flakes │ │ ├── prerequisites.md │ │ └── standalone.md │ ├── contributing │ │ ├── getting-started.md │ │ └── news.md │ ├── installation.md │ ├── introduction.md │ ├── usage.md │ └── manpage-urls.json ├── project-configuration-nix-footer.5 ├── highlight-style.css ├── release-notes │ ├── rl-0.md │ └── release-notes.md ├── options.html ├── project-configuration-nix-header.5 ├── html-open-tool.nix ├── project-manager-manual.nix ├── static │ ├── tomorrow-night.min.css │ └── tomorrow.min.css └── default.nix ├── .github ├── renovate.json ├── workflows │ ├── flakestry-publish.yml │ ├── switch-pm-generation.yml │ ├── flakehub-publish.yml │ └── pages.yml └── settings.yml ├── .config ├── mustache.yaml └── project │ ├── README.md │ └── github-pages.nix ├── release.nix ├── nix ├── lib │ ├── default.nix │ ├── default-configuration.nix │ └── configuration.nix └── schemas.nix ├── nix-ci.nix ├── .vale.ini ├── testing ├── big-test.nix ├── asserts.nix ├── stubs.nix ├── darwinScrubList.nix ├── flake.nix └── default.nix ├── .cache └── vale │ ├── Vocab │ └── project-manager │ │ └── accept.txt │ └── config │ └── vocabularies │ └── project-manager │ └── accept.txt ├── .editorconfig ├── templates ├── default.nix └── default │ ├── flake.nix │ └── .config │ └── project │ └── default.nix ├── .gitattributes ├── garnix.yaml ├── LICENSE └── project-manager ├── build-news.nix ├── default.nix ├── completion.zsh └── completion.fish /modules/programs/git/tests/enabled/.gitattributes: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /modules/files/tests/simple/meh: -------------------------------------------------------------------------------- 1 | This is a very simple file. 2 | -------------------------------------------------------------------------------- /modules/files/tests/default.nix: -------------------------------------------------------------------------------- 1 | { 2 | files-simple = ./simple/default.nix; 3 | } 4 | -------------------------------------------------------------------------------- /modules/services/flakehub/tests/default.nix: -------------------------------------------------------------------------------- 1 | { 2 | flakehub-enabled = ./enabled; 3 | } 4 | -------------------------------------------------------------------------------- /docs/manual/3rd-party/collections.md: -------------------------------------------------------------------------------- 1 | # Module Collections {#sec-3rd-party-module-collections} 2 | -------------------------------------------------------------------------------- /docs/project-configuration-nix-footer.5: -------------------------------------------------------------------------------- 1 | .SH "AUTHORS" 2 | .PP 3 | Project Manager contributors 4 | -------------------------------------------------------------------------------- /modules/services/renovate/tests/default.nix: -------------------------------------------------------------------------------- 1 | { 2 | renovate-simple = ./simple/default.nix; 3 | } 4 | -------------------------------------------------------------------------------- /modules/misc/editorconfig/tests/default.nix: -------------------------------------------------------------------------------- 1 | { 2 | editorconfig-simple = ./simple/default.nix; 3 | } 4 | -------------------------------------------------------------------------------- /modules/programs/git/tests/default.nix: -------------------------------------------------------------------------------- 1 | { 2 | git-enabled = ./enabled; 3 | git-settings = ./settings; 4 | } 5 | -------------------------------------------------------------------------------- /docs/highlight-style.css: -------------------------------------------------------------------------------- 1 | pre { 2 | padding: 0; 3 | } 4 | 5 | pre code.hljs { 6 | border: none; 7 | margin: 0; 8 | } 9 | -------------------------------------------------------------------------------- /modules/programs/git/tests/enabled/gitignore: -------------------------------------------------------------------------------- 1 | /.gitattributes 2 | /.gitignore 3 | /.local/state/project-manager/ 4 | /.local/state/nix/profiles/ 5 | -------------------------------------------------------------------------------- /docs/release-notes/rl-0.md: -------------------------------------------------------------------------------- 1 | # Release 0 {#sec-release-0} 2 | 3 | This is the current unstable branch and the information in this section 4 | is therefore not final. 5 | -------------------------------------------------------------------------------- /modules/programs/git/tests/settings/.gitattributes: -------------------------------------------------------------------------------- 1 | *.bin -diff important=50 2 | *.pdf diff=pdf 3 | /some-file linguist-generated 4 | meh important=100 5 | other !unset 6 | -------------------------------------------------------------------------------- /modules/services/renovate/tests/simple/renovate.json: -------------------------------------------------------------------------------- 1 | {"$schema":"https://docs.renovatebot.com/renovate-schema.json","extends":["config:recommended"],"nix":{"enabled":true}} -------------------------------------------------------------------------------- /modules/programs/git/tests/settings/gitignore: -------------------------------------------------------------------------------- 1 | /.gitattributes 2 | /.gitignore 3 | /.local/state/project-manager/ 4 | /.local/state/nix/profiles/ 5 | /.locally-generated 6 | /.cache 7 | -------------------------------------------------------------------------------- /docs/manual/options.md: -------------------------------------------------------------------------------- 1 | # Project Manager Configuration Options {#ch-options} 2 | 3 | ```{=include=} options 4 | id-prefix: opt- 5 | list-id: project-manager-options 6 | source: @OPTIONS_JSON@ 7 | ``` 8 | -------------------------------------------------------------------------------- /modules/lib/types/tests/default.nix: -------------------------------------------------------------------------------- 1 | { 2 | lib-types-dag-submodule = ./dag-submodule.nix; 3 | lib-types-dag-merge = ./dag-merge.nix; 4 | 5 | lib-types-gvariant-merge = ./gvariant-merge.nix; 6 | } 7 | -------------------------------------------------------------------------------- /docs/manual/faq.md: -------------------------------------------------------------------------------- 1 | # Frequently Asked Questions (FAQ) {#ch-faq} 2 | 3 | ```{=include=} sections 4 | faq/collision.md 5 | faq/session-variables.md 6 | faq/ca-desrt-dconf.md 7 | faq/unstable.md 8 | faq/change-package-module.md 9 | ``` 10 | -------------------------------------------------------------------------------- /docs/release-notes/release-notes.md: -------------------------------------------------------------------------------- 1 | # Release Notes {#ch-release-notes} 2 | 3 | This section lists the release notes for stable versions of Project Manager 4 | and the current unstable version. 5 | 6 | ```{=include=} chapters 7 | rl-0.md 8 | ``` 9 | -------------------------------------------------------------------------------- /modules/files/tests/simple/default.nix: -------------------------------------------------------------------------------- 1 | { 2 | project.file."meh".text = '' 3 | This is a very simple file. 4 | ''; 5 | 6 | nmt.script = '' 7 | assertFileExists "project-files/meh" 8 | assertFileContent "project-files/meh" ${./meh} 9 | ''; 10 | } 11 | -------------------------------------------------------------------------------- /modules/programs/git/tests/settings/git/config: -------------------------------------------------------------------------------- 1 | ; This file was generated by Project Manager. 2 | [include] 3 | path = "~/some-extra/git/config.inc" 4 | 5 | ; This file was generated by Project Manager. 6 | [includeIf "gitdir:~/src/dir"] 7 | path = "~/some-extra/git/conditional.inc" 8 | -------------------------------------------------------------------------------- /docs/manual/faq/session-variables.md: -------------------------------------------------------------------------------- 1 | 2 | # Why are the session variables not set? {#_why_are_the_session_variables_not_set} 3 | 4 | Project Manager is only able to set session variables automatically in derivations that extend the ones provided by Project Manager. 5 | -------------------------------------------------------------------------------- /.github/renovate.json: -------------------------------------------------------------------------------- 1 | {"$schema":"https://docs.renovatebot.com/renovate-schema.json","extends":["config:recommended"],"labels":["automated"],"lockFileMaintenance":{"automerge":true,"enabled":true},"nix":{"enabled":true},"packageRules":[{"automerge":true,"matchCurrentVersion":"!/^0/","matchUpdateTypes":["minor","patch"]}]} 2 | -------------------------------------------------------------------------------- /.config/mustache.yaml: -------------------------------------------------------------------------------- 1 | { 2 | "project": 3 | { 4 | "name": "project-manager", 5 | "summary": "Home Manager, but for repos.", 6 | "description": "A configuration for managing flake-based projects.", 7 | "repo": "sellout/project-manager", 8 | }, 9 | "type": { "name": "nix" }, 10 | } 11 | -------------------------------------------------------------------------------- /modules/misc/editorconfig/tests/simple/editorconfig: -------------------------------------------------------------------------------- 1 | # This file was generated by Project Manager. 2 | root=true 3 | 4 | [*] 5 | charset=utf-8 6 | end_of_line=lf 7 | indent_size=2 8 | indent_style=space 9 | insert_final_newline=true 10 | max_line_width=78 11 | trim_trailing_whitespace=true 12 | 13 | [*.rs] 14 | indent_size=4 15 | -------------------------------------------------------------------------------- /modules/services/renovate/tests/simple/default.nix: -------------------------------------------------------------------------------- 1 | { 2 | ## With no additional settings, we get a bare-bones Renovate config. 3 | services.renovate.enable = true; 4 | 5 | nmt.script = '' 6 | assertFileExists "project-files/renovate.json" 7 | assertFileContent "project-files/renovate.json" ${./renovate.json} 8 | ''; 9 | } 10 | -------------------------------------------------------------------------------- /release.nix: -------------------------------------------------------------------------------- 1 | let 2 | ## This follows Semantic Versioning 2.0.0 (https://semver.org/) 3 | version = { 4 | major = 0; 5 | minor = 7; 6 | patch = 0; 7 | }; 8 | in { 9 | inherit version; 10 | release = ( 11 | with version; "${toString major}.${toString minor}.${toString patch}" 12 | ); 13 | isReleaseBranch = true; 14 | } 15 | -------------------------------------------------------------------------------- /nix/lib/default.nix: -------------------------------------------------------------------------------- 1 | { 2 | flake-utils, 3 | pkgsFor, 4 | self, 5 | treefmt-nix, 6 | }: let 7 | configuration = import ./configuration.nix { 8 | inherit flake-utils pkgsFor self treefmt-nix; 9 | }; 10 | in { 11 | inherit configuration; 12 | 13 | defaultConfiguration = 14 | import ./default-configuration.nix {inherit configuration;}; 15 | } 16 | -------------------------------------------------------------------------------- /nix-ci.nix: -------------------------------------------------------------------------------- 1 | # This file was generated by Project Manager. 2 | { allow-import-from-derivation = false; auto-retry = true; build-logs = true; cachix = { name = "sellout"; public-key = "sellout.cachix.org-1:v37cTpWBEycnYxSPAgSQ57Wiqd3wjljni2aC0Xry1DE="; }; deploy = { }; doNotBuild = [ "devShells.x86_64-linux.lax-checks" ]; enable = true; fail-fast = false; impure = false; test = { }; } 3 | -------------------------------------------------------------------------------- /.vale.ini: -------------------------------------------------------------------------------- 1 | ; This file was generated by Project Manager. 2 | MinAlertLevel=suggestion 3 | Packages=Microsoft 4 | StylesPath=.cache/vale 5 | Vocab=project-manager 6 | 7 | [*] 8 | BasedOnStyles=Vale, Microsoft 9 | Microsoft.Dashes=NO 10 | Microsoft.GeneralURL=NO 11 | Microsoft.Headings=NO 12 | Microsoft.Quotes=NO 13 | Microsoft.Ranges=NO 14 | Microsoft.Vocab=NO 15 | Microsoft.We=NO 16 | -------------------------------------------------------------------------------- /docs/manual/usage/updating.md: -------------------------------------------------------------------------------- 1 | # Updating {#sec-updating} 2 | 3 | If you have installed Project Manager using the Nix channel method then 4 | updating Project Manager is done by first updating the channel. You can 5 | then switch to the updated Project Manager environment. 6 | 7 | ```shell 8 | $ nix-channel --update 9 | … 10 | unpacking channels... 11 | $ project-manager switch 12 | ``` 13 | -------------------------------------------------------------------------------- /testing/big-test.nix: -------------------------------------------------------------------------------- 1 | {lib, ...}: { 2 | options.test.enableBig = lib.mkOption { 3 | type = lib.types.bool; 4 | default = true; 5 | description = '' 6 | Whether to enable "big" tests. These are tests that require 7 | more resources than typical tests. For example, tests that depend on large 8 | packages or tests that take long to run. 9 | ''; 10 | }; 11 | } 12 | -------------------------------------------------------------------------------- /modules/lib/types/tests/dag-merge-result.txt: -------------------------------------------------------------------------------- 1 | before:before 2 | merged:left,middle,middle,right 3 | between:between 4 | after:after 5 | list-anywhere-0:list-anywhere-0 6 | list-before-0:list-before-0,sneaky-merge 7 | list-before-1:list-before-1 8 | list-anywhere-1:list-anywhere-1 9 | inside-list:inside-list 10 | list-after-0:list-after-0 11 | list-after-1:list-after-1 12 | list-anywhere-2:list-anywhere-2 13 | -------------------------------------------------------------------------------- /.cache/vale/Vocab/project-manager/accept.txt: -------------------------------------------------------------------------------- 1 | Hacktoberfest 2 | direnv 3 | formatter 4 | garnix 5 | [Nn]ix 6 | Pfeil 7 | ShellCheck 8 | alejandra 9 | babelfish 10 | [Bb]oolean 11 | composable 12 | DBus 13 | dconf 14 | declutter 15 | decluttered 16 | decluttering 17 | devenv 18 | devShell 19 | Dhall 20 | formatters 21 | NixOS 22 | Nixpkgs 23 | NMT 24 | sandboxed 25 | subcommands 26 | systemd 27 | treefmt 28 | unsandboxed 29 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # This file was generated by Project Manager. 2 | root=true 3 | 4 | [*] 5 | binary_next_line=true 6 | charset=utf-8 7 | end_of_line=lf 8 | indent_size=2 9 | indent_style=space 10 | insert_final_newline=true 11 | space_redirects=true 12 | switch_case_indent=true 13 | trim_trailing_whitespace=true 14 | 15 | [*.{diff,patch}] 16 | trim_trailing_whitespace=false 17 | 18 | [*.{el,lisp}] 19 | indent_size=unset 20 | -------------------------------------------------------------------------------- /templates/default.nix: -------------------------------------------------------------------------------- 1 | { 2 | default = { 3 | description = "A simple flake with project configuration"; 4 | path = ./default; 5 | welcomeText = '' 6 | You now have a flake with a centralized project configuration! 7 | 8 | 1. edit the project configuration in “.config/project/default.nix”. 9 | 2. use `nix run .#project-manager -- switch` to populate the repo. 10 | ''; 11 | }; 12 | } 13 | -------------------------------------------------------------------------------- /modules/lib/shell.nix: -------------------------------------------------------------------------------- 1 | {lib}: rec { 2 | # Produces a Bourne shell like variable export statement. 3 | export = n: v: ''export ${n}="${toString v}"''; 4 | 5 | # Given an attribute set containing shell variable names and their 6 | # assignment, this function produces a string containing an export 7 | # statement for each set entry. 8 | exportAll = vars: lib.concatStringsSep "\n" (lib.mapAttrsToList export vars); 9 | } 10 | -------------------------------------------------------------------------------- /.cache/vale/config/vocabularies/project-manager/accept.txt: -------------------------------------------------------------------------------- 1 | Hacktoberfest 2 | direnv 3 | formatter 4 | garnix 5 | [Nn]ix 6 | Pfeil 7 | ShellCheck 8 | alejandra 9 | babelfish 10 | [Bb]oolean 11 | composable 12 | DBus 13 | dconf 14 | declutter 15 | decluttered 16 | decluttering 17 | devenv 18 | devShell 19 | Dhall 20 | formatters 21 | NixOS 22 | Nixpkgs 23 | NMT 24 | sandboxed 25 | subcommands 26 | systemd 27 | treefmt 28 | unsandboxed 29 | -------------------------------------------------------------------------------- /modules/services/flakehub/tests/enabled/default.nix: -------------------------------------------------------------------------------- 1 | { 2 | services = { 3 | flakehub = { 4 | enable = true; 5 | name = "sellout/example"; 6 | }; 7 | github.enable = true; 8 | }; 9 | 10 | nmt.script = '' 11 | assertFileExists "project-files/.github/workflows/flakehub-publish.yml" 12 | assertFileContent "project-files/.github/workflows/flakehub-publish.yml" \ 13 | ${./.github/workflows/flakehub-publish.yml} 14 | ''; 15 | } 16 | -------------------------------------------------------------------------------- /modules/lib/stdlib-extended.nix: -------------------------------------------------------------------------------- 1 | # Just a convenience function that returns the given Nixpkgs standard 2 | # library extended with the HM library. 3 | nixpkgsLib: let 4 | mkPmLib = import ./.; 5 | in 6 | nixpkgsLib.extend (self: super: { 7 | pm = mkPmLib {lib = self;}; 8 | 9 | # For forward compatibility. 10 | literalExpression = super.literalExpression or super.literalExample; 11 | literalDocBook = super.literalDocBook or super.literalExample; 12 | }) 13 | -------------------------------------------------------------------------------- /modules/programs/git/tests/enabled/default.nix: -------------------------------------------------------------------------------- 1 | { 2 | programs.git.enable = true; 3 | 4 | nmt.script = '' 5 | assertFileExists project-files/.gitattributes 6 | assertFileContent project-files/.gitattributes ${./.gitattributes} 7 | assertFileExists project-files/.gitignore 8 | ## NB: We remove the leading “.” from the filename so that it doesn’t 9 | ## actually ignore things. 10 | assertFileContent project-files/.gitignore ${./gitignore} 11 | ''; 12 | } 13 | -------------------------------------------------------------------------------- /docs/manual/introduction/devShell.md: -------------------------------------------------------------------------------- 1 | # the `project-manager` devShell {#sec-project-manager-devShell} 2 | 3 | One of the most powerful combinations is `self.projectConfigurations.${system}.devShells.project-manager`. This provides a shell that has a PATH and other environment that have been produced by the Project Manager configuration. If you use this environment, you can often avoid producing files into your working tree, as the executables available here may be modified to look for their files directly in the Nix store. 4 | -------------------------------------------------------------------------------- /docs/manual/quick-start.md: -------------------------------------------------------------------------------- 1 | # Quick Start {#ch-quick-start} 2 | 3 | It’s not necessary to install Project Manager to use it for a project. The easiest way to try it out is on a new project, but it’s also intended to be easy to integrate it with an existing project 4 | 5 | **NB**: If you have installed Project Manager already, then `nix run github:sellout/project-manager --` can be replaced with `project-manager` in any commands. 6 | 7 | ```{=include=} sections 8 | quick-start/new-project.md 9 | quick-start/existing-project.md 10 | ``` 11 | -------------------------------------------------------------------------------- /docs/options.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Redirecting… 4 | 5 | 6 | 9 | 10 | 11 |

Redirecting…

12 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /docs/manual/manual.md: -------------------------------------------------------------------------------- 1 | # Project Manager Manual {#project-manager-manual} 2 | 3 | ## Version 0.7.0 (unstable) 4 | 5 | ```{=include=} preface 6 | preface.md 7 | ``` 8 | 9 | ```{=include=} parts 10 | introduction.md 11 | quick-start.md 12 | installation.md 13 | usage.md 14 | nix-flakes.md 15 | writing-modules.md 16 | contributing.md 17 | 3rd-party.md 18 | faq.md 19 | ``` 20 | 21 | ```{=include=} appendix html:into-file=//options.xhtml 22 | options.md 23 | ``` 24 | 25 | ```{=include=} appendix html:into-file=//release-notes.xhtml 26 | release-notes/release-notes.md 27 | ``` 28 | -------------------------------------------------------------------------------- /docs/manual/quick-start/new-project.md: -------------------------------------------------------------------------------- 1 | # New Project {#sec-new-project} 2 | 3 | To create a new project from scratch, run the following command : 4 | 5 | ```bash 6 | nix run github:sellout/project-manager -- init my-new-project --switch 7 | ``` 8 | 9 | This will create a new directory called “my-new-project” in the current directory, create a flake and project configuration (.config/project/default.nix) in that directory, and populate it with files as dictated by the project configuration. 10 | 11 | See [§Configuration](#sec-usage-configuration) for where to go from here. 12 | -------------------------------------------------------------------------------- /docs/manual/3rd-party.md: -------------------------------------------------------------------------------- 1 | # Third-Party Tools and Extensions {#ch-3rd-party} 2 | 3 | Here is a collection of tools and extensions that relate to Project 4 | Manager. Note, these are maintained outside the regular Project Manager 5 | flow so quality and support may vary wildly. If you encounter problems 6 | then please raise them in the corresponding project, not as issues in 7 | the Project Manager tracker. 8 | 9 | If you have made something interesting related to Project Manager then you 10 | are encouraged to create a PR that expands this chapter. 11 | 12 | ```{=include=} sections 13 | 3rd-party/collections.md 14 | ``` 15 | -------------------------------------------------------------------------------- /modules/lib/default.nix: -------------------------------------------------------------------------------- 1 | {lib}: rec { 2 | dag = import ./dag.nix {inherit lib;}; 3 | 4 | # assertions = import ./assertions.nix { inherit lib; }; 5 | 6 | # booleans = import ./booleans.nix { inherit lib; }; 7 | generators = import ./generators.nix {inherit lib;}; 8 | gvariant = import ./gvariant.nix {inherit lib;}; 9 | # maintainers = import ./maintainers.nix; 10 | path = import ./path.nix {inherit lib;}; 11 | strings = import ./strings.nix {inherit lib;}; 12 | types = import ./types {inherit gvariant lib;}; 13 | 14 | shell = import ./shell.nix {inherit lib;}; 15 | # zsh = import ./zsh.nix { inherit lib; }; 16 | } 17 | -------------------------------------------------------------------------------- /docs/manual/nix-flakes.md: -------------------------------------------------------------------------------- 1 | # Nix Flakes {#ch-nix-flakes} 2 | 3 | Project Manager is compatible with [Nix 4 | Flakes](https://wiki.nixos.org/wiki/Flakes). But please be aware that this 5 | support is still experimental and may change in backwards 6 | incompatible ways. 7 | 8 | Just like in the standard installation you can use the Project Manager 9 | flake in one way: 10 | 11 | 1. Using the standalone `project-manager` tool. See 12 | [Standalone setup](#sec-flakes-standalone) for instructions on how 13 | to perform this installation. 14 | 15 | ```{=include=} sections 16 | nix-flakes/prerequisites.md 17 | nix-flakes/standalone.md 18 | ``` 19 | -------------------------------------------------------------------------------- /docs/manual/preface.md: -------------------------------------------------------------------------------- 1 | # Preface {#preface} 2 | 3 | This manual will eventually describe how to install, use, and extend Project 4 | Manager. 5 | 6 | If you encounter problems then please reach out on [the Matrix room](https://matrix.to/#/%23project-manager:matrix.org). 7 | If your problem is caused by a bug in Project Manager then it should 8 | be reported on the 9 | [Project Manager issue tracker](https://github.com/nix-community/project-manager/issues). 10 | 11 | :::{.note} 12 | Commands prefixed with `$ sudo` have to be run as root, either 13 | requiring to login as root user or temporarily switching to it using 14 | `sudo` for example. 15 | ::: 16 | -------------------------------------------------------------------------------- /modules/misc/editorconfig/tests/simple/default.nix: -------------------------------------------------------------------------------- 1 | { 2 | editorconfig = { 3 | enable = true; 4 | settings = { 5 | "*" = { 6 | charset = "utf-8"; 7 | end_of_line = "lf"; 8 | trim_trailing_whitespace = true; 9 | insert_final_newline = true; 10 | max_line_width = 78; 11 | indent_style = "space"; 12 | indent_size = 2; 13 | }; 14 | "*.rs" = { 15 | indent_size = 4; 16 | }; 17 | }; 18 | }; 19 | 20 | nmt.script = '' 21 | assertFileExists "project-files/.editorconfig" 22 | assertFileContent "project-files/.editorconfig" ${./editorconfig} 23 | ''; 24 | } 25 | -------------------------------------------------------------------------------- /nix/lib/default-configuration.nix: -------------------------------------------------------------------------------- 1 | ## Takes the same arguments as `configuration`, but defaults to loading 2 | ## configuration from `$PROJECT_ROOT/.config/project` and 3 | ## `$PROJECT_ROOT/.config/project/user`. 4 | {configuration}: { 5 | self, 6 | modules ? [], 7 | ... 8 | } @ args: let 9 | projectConfig = "${self}/.config/project"; 10 | userConfig = "${projectConfig}/user"; 11 | in 12 | configuration 13 | (args 14 | // { 15 | modules = 16 | modules 17 | ++ [projectConfig] 18 | ++ ( 19 | if builtins.pathExists userConfig 20 | then [userConfig] 21 | else [] 22 | ); 23 | }) 24 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | /.cache/vale/Vocab/project-manager/accept.txt linguist-generated 2 | /.cache/vale/config/vocabularies/project-manager/accept.txt linguist-generated 3 | /.editorconfig linguist-generated 4 | /.gitattributes linguist-generated 5 | /.github/renovate.json linguist-generated 6 | /.github/settings.yml linguist-generated 7 | /.github/workflows/flakehub-publish.yml linguist-generated 8 | /.github/workflows/flakestry-publish.yml linguist-generated 9 | /.github/workflows/pages.yml linguist-generated 10 | /.github/workflows/switch-pm-generation.yml linguist-generated 11 | /.vale.ini linguist-generated 12 | /garnix.yaml linguist-generated 13 | /nix-ci.nix linguist-generated 14 | -------------------------------------------------------------------------------- /docs/manual/writing-modules.md: -------------------------------------------------------------------------------- 1 | # Writing Project Manager Modules {#ch-writing-modules} 2 | 3 | The module system in Project Manager is based entirely on the NixOS module 4 | system so we will here only highlight aspects that are specific for Project 5 | Manager. For information about the module system as such please refer to 6 | the [Writing NixOS 7 | Modules](https://nixos.org/nixos/manual/index.html#sec-writing-modules) 8 | chapter of the NixOS manual. 9 | 10 | Overall the basic option types are the same in Project Manager as Home Manager. There are some extra options provided in the file-type submodule. 11 | 12 | ```{=include=} sections 13 | writing-modules/types.md 14 | ``` 15 | -------------------------------------------------------------------------------- /.github/workflows/flakestry-publish.yml: -------------------------------------------------------------------------------- 1 | name: "Publish a flake to flakestry" 2 | on: 3 | push: 4 | tags: 5 | - "v?[0-9]+.[0-9]+.[0-9]+" 6 | - "v?[0-9]+.[0-9]+" 7 | workflow_dispatch: 8 | inputs: 9 | tag: 10 | description: "The existing tag to publish" 11 | type: "string" 12 | required: true 13 | jobs: 14 | publish-flake: 15 | runs-on: ubuntu-24.04 16 | permissions: 17 | id-token: "write" 18 | contents: "read" 19 | steps: 20 | - uses: flakestry/flakestry-publish@main 21 | with: 22 | version: "${{ inputs.tag || github.ref_name }}" 23 | -------------------------------------------------------------------------------- /.github/workflows/switch-pm-generation.yml: -------------------------------------------------------------------------------- 1 | # This file was generated by Project Manager. 2 | {"jobs":{"switch":{"if":"github.head_ref == 'renovate/lock-file-maintenance'","runs-on":"ubuntu-24.04","steps":[{"uses":"actions/checkout@v6","with":{"ref":"${{ github.event.pull_request.head.ref }}}","repository":"${{ github.event.pull_request.head.repo.full_name }}","token":"${{ secrets.PROJECT_MANAGER_TOKEN }}"}},{"uses":"cachix/install-nix-action@v31"},{"run":"nix develop .#project-manager --command project-manager kitchen-sink"},{"name":"commit changes","uses":"EndBug/add-and-commit@v9","with":{"add":"--all","default_author":"github_actions","message":"Switch Project Manager generation"}}]}},"name":"Project Manager","on":{"pull_request":{}}} 3 | -------------------------------------------------------------------------------- /docs/project-configuration-nix-header.5: -------------------------------------------------------------------------------- 1 | .TH "PROJECT-CONFIGURATION\&.NIX" "5" "01/01/1980" "Project Manager" 2 | .\" disable hyphenation 3 | .nh 4 | .\" disable justification (adjust text to left margin only) 5 | .ad l 6 | .\" enable line breaks after slashes 7 | .cflags 4 / 8 | .SH "NAME" 9 | \fIproject\-configuration\&.nix\fP \- Project Manager configuration specification 10 | .SH "DESCRIPTION" 11 | .sp 12 | The file ~/\&.config/project/default\&.nix contains the declarative specification of your Project Manager configuration\&. The command \fBproject\-manager\fR takes this file and realises the user environment configuration specified therein\&. 13 | .SH "OPTIONS" 14 | .PP 15 | You can use the following options in 16 | project\-configuration\&.nix: 17 | .PP 18 | -------------------------------------------------------------------------------- /docs/manual/faq/ca-desrt-dconf.md: -------------------------------------------------------------------------------- 1 | 2 | # Why do I get an error message about `ca.desrt.dconf` or `dconf.service`? {#_why_do_i_get_an_error_message_about_literal_ca_desrt_dconf_literal_or_literal_dconf_service_literal} 3 | 4 | You are most likely trying to configure something that uses dconf but 5 | the DBus session isn’t aware of the dconf service. The full error you 6 | might get is 7 | 8 | error: GDBus.Error:org.freedesktop.DBus.Error.ServiceUnknown: The name ca.desrt.dconf was not provided by any .service files 9 | 10 | or 11 | 12 | error: GDBus.Error:org.freedesktop.systemd1.NoSuchUnit: Unit dconf.service not found. 13 | 14 | The solution on NixOS is to add 15 | 16 | ```nix 17 | programs.dconf.enable = true; 18 | ``` 19 | 20 | to your system configuration. 21 | -------------------------------------------------------------------------------- /garnix.yaml: -------------------------------------------------------------------------------- 1 | # This file was generated by Project Manager. 2 | {"builds":[{"exclude":["devShells.aarch64-darwin.lax-checks","devShells.aarch64-linux.lax-checks","devShells.i686-linux.lax-checks","devShells.x86_64-linux.lax-checks","checks.*.formatter-22_11","checks.*.formatter-23_05","checks.*.formatter-23_11","checks.*.formatter-24_05","checks.*.formatter-24_11","checks.*.shellcheck-22_11","checks.*.shellcheck-23_05","checks.*.shellcheck-23_11","checks.*.shellcheck-24_05","checks.*.shellcheck-24_11","checks.aarch64-linux.formatter","checks.i686-linux.formatter","checks.x86_64-linux.formatter","checks.aarch64-linux.shellcheck","checks.i686-linux.shellcheck","checks.x86_64-linux.shellcheck","checks.aarch64-linux.vale","checks.i686-linux.vale","checks.x86_64-linux.vale","*.x86_64-darwin","*.x86_64-darwin.*","*.x86_64-darwin-example","*.x86_64-linux.*"],"include":["*.*","*.*.*"]}],"servers":[]} 3 | -------------------------------------------------------------------------------- /.github/workflows/flakehub-publish.yml: -------------------------------------------------------------------------------- 1 | name: "Publish tags to FlakeHub" 2 | on: 3 | push: 4 | tags: 5 | - "v?[0-9]+.[0-9]+.[0-9]+*" 6 | workflow_dispatch: 7 | inputs: 8 | tag: 9 | description: "The existing tag to publish to FlakeHub" 10 | type: "string" 11 | required: true 12 | jobs: 13 | flakehub-publish: 14 | runs-on: "ubuntu-24.04" 15 | permissions: 16 | id-token: "write" 17 | contents: "read" 18 | steps: 19 | - uses: "actions/checkout@v6" 20 | with: 21 | ref: "${{ (inputs.tag != null) && format('refs/tags/{0}', inputs.tag) || '' }}" 22 | - uses: "DeterminateSystems/nix-installer-action@main" 23 | - uses: "DeterminateSystems/flakehub-push@main" 24 | with: 25 | visibility: "public" 26 | name: "sellout/project-manager" 27 | tag: "${{ inputs.tag }}" 28 | -------------------------------------------------------------------------------- /modules/lib/strings.nix: -------------------------------------------------------------------------------- 1 | {lib}: let 2 | inherit 3 | (lib) 4 | genList 5 | length 6 | lowerChars 7 | replaceStrings 8 | stringToCharacters 9 | upperChars 10 | ; 11 | in { 12 | # Figures out a valid Nix store name for the given path. 13 | storeFileName = path: let 14 | # All characters that are considered safe. Note "-" is not 15 | # included to avoid "-" followed by digit being interpreted as a 16 | # version. 17 | safeChars = 18 | ["+" "." "_" "?" "="] 19 | ++ lowerChars 20 | ++ upperChars 21 | ++ stringToCharacters "0123456789"; 22 | 23 | empties = l: genList (x: "") (length l); 24 | 25 | unsafeInName = 26 | stringToCharacters (replaceStrings safeChars (empties safeChars) path); 27 | 28 | safeName = replaceStrings unsafeInName (empties unsafeInName) path; 29 | in 30 | "pm_" + safeName; 31 | } 32 | -------------------------------------------------------------------------------- /.config/project/README.md: -------------------------------------------------------------------------------- 1 | # Example project configuration 2 | 3 | This isn’t only an example, but the actual project configuration used by this repository. See [the top-level README](../README.md) for more about what Project Manager is and how to use it. 4 | 5 | The key file here is [default.nix](./default.nix), which is explicitly imported by [the top-level flake.nix](../../flake.nix:). Everything else in this directory is imported by default.nix. default.nix has the general project metadata, and the other files contain configuration for various other programs and services that this repository relies on. 6 | 7 | This organization isn’t required. All these files could be combined, or perhaps just one or two large configurations extracted from default.nix. default.nix itself can be anywhere in your repository and named anything, so long as it’s referenced correctly in `projectConfigurations` in flake.nix. 8 | -------------------------------------------------------------------------------- /modules/services/flakehub/tests/enabled/.github/workflows/flakehub-publish.yml: -------------------------------------------------------------------------------- 1 | # This file was generated by Project Manager. 2 | name: "Publish tags to FlakeHub" 3 | on: 4 | push: 5 | tags: 6 | - "v?[0-9]+.[0-9]+.[0-9]+*" 7 | workflow_dispatch: 8 | inputs: 9 | tag: 10 | description: "The existing tag to publish to FlakeHub" 11 | type: "string" 12 | required: true 13 | jobs: 14 | flakehub-publish: 15 | runs-on: "ubuntu-24.04" 16 | permissions: 17 | id-token: "write" 18 | contents: "read" 19 | steps: 20 | - uses: "actions/checkout@v6" 21 | with: 22 | ref: "${{ (inputs.tag != null) && format('refs/tags/{0}', inputs.tag) || '' }}" 23 | - uses: "DeterminateSystems/nix-installer-action@main" 24 | - uses: "DeterminateSystems/flakehub-push@main" 25 | with: 26 | visibility: "unlisted" 27 | name: "sellout/example" 28 | tag: "${{ inputs.tag }}" 29 | -------------------------------------------------------------------------------- /.github/workflows/pages.yml: -------------------------------------------------------------------------------- 1 | # This file was generated by Project Manager. 2 | {"concurrency":{"cancel-in-progress":false,"group":"pages"},"jobs":{"build":{"runs-on":"ubuntu-24.04","steps":[{"name":"Checkout","uses":"actions/checkout@v6"},{"name":"Setup Pages","uses":"actions/configure-pages@v4"},{"uses":"cachix/install-nix-action@v24","with":{"extra_nix_config":"extra-trusted-public-keys = cache.garnix.io:CTFPyKSLcx5RMJKfLo5EEPUObbA78b0YQ2DTCJXqr9g=\nextra-substituters = https://cache.garnix.io\n"}},{"run":"nix build .#docs-html\ncp -r result/share/doc/project-manager ./_site\n"},{"name":"Upload artifact","uses":"actions/upload-pages-artifact@v3"}]},"deploy":{"environment":{"name":"github-pages","url":"${{ steps.deployment.outputs.page_url }}"},"needs":"build","runs-on":"ubuntu-24.04","steps":[{"id":"deployment","name":"Deploy to GitHub Pages","uses":"actions/deploy-pages@v4"}]}},"name":"Deploy generated docs to Pages","on":{"push":{"branches":["main"]},"workflow_dispatch":{}},"permissions":{"contents":"read","id-token":"write","pages":"write"}} 3 | -------------------------------------------------------------------------------- /docs/manual/faq/unstable.md: -------------------------------------------------------------------------------- 1 | 2 | # How do I install packages from Nixpkgs unstable? {#_how_do_i_install_packages_from_nixpkgs_unstable} 3 | 4 | If you are using a stable version of Nixpkgs but would like to install 5 | some particular packages from Nixpkgs unstable – or some other channel 6 | – then you can import the unstable Nixpkgs and refer to its packages 7 | within your configuration. Something like 8 | 9 | ```nix 10 | { pkgs, config, ... }: 11 | 12 | let 13 | 14 | pkgsUnstable = import {}; 15 | 16 | in 17 | 18 | { 19 | project.devPackages = [ 20 | pkgsUnstable.foo 21 | ]; 22 | 23 | # … 24 | } 25 | ``` 26 | 27 | should work provided you have a Nix channel called `nixpkgs-unstable`. 28 | 29 | You can add the `nixpkgs-unstable` channel by running 30 | 31 | ```shell 32 | $ nix-channel --add https://nixos.org/channels/nixpkgs-unstable nixpkgs-unstable 33 | $ nix-channel --update 34 | ``` 35 | 36 | Note, the package won’t be affected by any package overrides, 37 | overlays, etc. 38 | -------------------------------------------------------------------------------- /nix/lib/configuration.nix: -------------------------------------------------------------------------------- 1 | { 2 | flake-utils, 3 | pkgsFor, 4 | self, 5 | treefmt-nix, 6 | }: let 7 | project-manager = self; 8 | in 9 | { 10 | self, 11 | modules ? [], 12 | pkgs, 13 | lib ? pkgs.lib, 14 | extraSpecialArgs ? {}, 15 | check ? true, 16 | supportedSystems ? flake-utils.lib.defaultSystems, 17 | }: 18 | import ../../modules { 19 | inherit check extraSpecialArgs lib pkgs; 20 | configuration = { 21 | imports = 22 | modules 23 | ++ [ 24 | { 25 | _module.args = { 26 | inherit 27 | project-manager 28 | self 29 | supportedSystems 30 | treefmt-nix 31 | ; 32 | ## The pkgs used by Project Manager itself, also used in modules 33 | ## in certain cases. 34 | pmPkgs = pkgsFor pkgs.system; 35 | }; 36 | programs.project-manager.path = toString ../.; 37 | } 38 | ]; 39 | }; 40 | modules = builtins.attrValues project-manager.projectModules; 41 | } 42 | -------------------------------------------------------------------------------- /modules/services/renovate/default.nix: -------------------------------------------------------------------------------- 1 | { 2 | config, 3 | lib, 4 | ... 5 | }: let 6 | cfg = config.services.renovate; 7 | in { 8 | meta.maintainers = [lib.maintainers.sellout]; 9 | 10 | options.services.renovate = { 11 | enable = lib.mkEnableOption "Renovate"; 12 | 13 | settings = lib.mkOption { 14 | type = lib.types.attrs; 15 | default = {}; 16 | description = '' 17 | Configuration written to {file}`$PROJECT_ROOT/renovate.json`. 18 | See for documentation. 19 | ''; 20 | }; 21 | }; 22 | 23 | config = lib.mkIf cfg.enable (let 24 | ## Renovate looks in multiple places for the configuration, so, when 25 | ## possible, we hide it in a directory that will already exist. 26 | location = 27 | if config.services.github.enable 28 | then ".github" 29 | else "."; 30 | in { 31 | project.file."${location}/renovate.json".text = lib.pm.generators.toJSON {} ({ 32 | "$schema" = "https://docs.renovatebot.com/renovate-schema.json"; 33 | extends = ["config:recommended"]; 34 | nix.enabled = true; 35 | } 36 | // cfg.settings); 37 | }); 38 | } 39 | -------------------------------------------------------------------------------- /modules/programs/git/tests/settings/default.nix: -------------------------------------------------------------------------------- 1 | { 2 | programs.git = { 3 | enable = true; 4 | attributes = { 5 | "/some-file".linguist-generated = true; 6 | "*.pdf".diff = "pdf"; 7 | meh.important = 100; 8 | "*.bin" = { 9 | diff = false; 10 | important = 50; 11 | }; 12 | other.unset = null; 13 | }; 14 | 15 | includes = [ 16 | {path = "~/some-extra/git/config.inc";} 17 | { 18 | path = "~/some-extra/git/conditional.inc"; 19 | condition = "gitdir:~/src/dir"; 20 | } 21 | ]; 22 | 23 | ignores = [ 24 | "/.locally-generated" 25 | "/.cache" 26 | ]; 27 | }; 28 | 29 | ## NB: We remove the leading “.” from the filename so that it doesn’t 30 | ## actually ignore things. 31 | nmt.script = '' 32 | assertFileExists "project-files/.git/config" 33 | assertFileContent "project-files/.git/config" ${./git/config} 34 | assertFileExists "project-files/.gitattributes" 35 | assertFileContent "project-files/.gitattributes" ${./.gitattributes} 36 | assertFileExists "project-files/.gitignore" 37 | assertFileContent "project-files/.gitignore" ${./gitignore} 38 | ''; 39 | } 40 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017-2023 Home Manager contributors 4 | Copyright (c) 2023 Project Manager contributors 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | -------------------------------------------------------------------------------- /docs/html-open-tool.nix: -------------------------------------------------------------------------------- 1 | { 2 | writeShellScriptBin, 3 | makeDesktopItem, 4 | symlinkJoin, 5 | }: { 6 | html, 7 | pathName ? "project-manager", 8 | projectName ? pathName, 9 | name ? "${pathName}-help", 10 | }: let 11 | helpScript = writeShellScriptBin name '' 12 | set -euo pipefail 13 | if [[ ! -v BROWSER || -z $BROWSER ]]; then 14 | for candidate in xdg-open open w3m; do 15 | BROWSER="$(type -P $candidate || true)" 16 | if [[ -x $BROWSER ]]; then 17 | break; 18 | fi 19 | done 20 | fi 21 | if [[ ! -v BROWSER || -z $BROWSER ]]; then 22 | echo "$0: unable to start a web browser; please set \$BROWSER" 23 | exit 1 24 | else 25 | exec "$BROWSER" "${html}/share/doc/${pathName}/index.xhtml" 26 | fi 27 | ''; 28 | 29 | desktopItem = makeDesktopItem { 30 | name = "${pathName}-manual"; 31 | desktopName = "${projectName} Manual"; 32 | genericName = "View ${projectName} documentation in a web browser"; 33 | icon = "nix-snowflake"; 34 | exec = "${helpScript}/bin/${name}"; 35 | categories = ["System"]; 36 | }; 37 | in 38 | symlinkJoin { 39 | inherit name; 40 | paths = [helpScript desktopItem]; 41 | } 42 | -------------------------------------------------------------------------------- /modules/all-modules.nix: -------------------------------------------------------------------------------- 1 | { 2 | modules, 3 | pkgs, 4 | # Note, this should be "the standard library" + PM extensions. 5 | lib, 6 | # Whether to enable module type checking. 7 | check ? true, 8 | }: let 9 | ## This includes modules from upstream projects, like nixpkgs. 10 | allModules = 11 | modules 12 | ++ [ 13 | ## FIXME: nixpkgs should expose these via flake outputs (and all of its 14 | ## modules) so we could instead have 15 | ## > nixpkgs.nixosModules.assertions 16 | ## > nixpkgs.nixosModules.lib 17 | ## > nixpkgs.nixosModules.meta 18 | ## or possibly even re-export them in _our_ `projectModules`. 19 | ## See NixOS/nixpkgs#???) 20 | (pkgs.path + "/nixos/modules/misc/assertions.nix") 21 | (pkgs.path + "/nixos/modules/misc/lib.nix") 22 | (pkgs.path + "/nixos/modules/misc/meta.nix") 23 | ]; 24 | in 25 | allModules 26 | ++ [ 27 | ({...}: { 28 | config = { 29 | _module.args.baseModules = allModules; 30 | _module.args.pkgsPath = lib.mkDefault pkgs.path; 31 | _module.args.pkgs = lib.mkDefault pkgs; 32 | _module.check = check; 33 | lib = lib.pm; 34 | }; 35 | }) 36 | ] 37 | -------------------------------------------------------------------------------- /modules/lib/types/tests/dag-submodule.nix: -------------------------------------------------------------------------------- 1 | { 2 | config, 3 | lib, 4 | pkgs, 5 | ... 6 | }: let 7 | inherit 8 | (lib) 9 | concatStringsSep 10 | pm 11 | mkOption 12 | types 13 | ; 14 | 15 | dag = lib.pm.dag; 16 | 17 | result = let 18 | sorted = dag.topoSort config.tested.dag; 19 | data = map (e: "${e.name}:${e.data.name}") sorted.result; 20 | in 21 | concatStringsSep "\n" data + "\n"; 22 | in { 23 | options.tested.dag = mkOption { 24 | type = pm.types.dagOf ( 25 | types.submodule ( 26 | {dagName, ...}: { 27 | options.name = mkOption {type = types.str;}; 28 | config.name = "dn-${dagName}"; 29 | } 30 | ) 31 | ); 32 | }; 33 | 34 | config = { 35 | tested.dag = { 36 | after = {}; 37 | before = dag.entryBefore ["after"] {}; 38 | between = dag.entryBetween ["after"] ["before"] {}; 39 | }; 40 | 41 | project.file."result.txt".text = result; 42 | 43 | nmt.script = '' 44 | assertFileContent \ 45 | project-files/result.txt \ 46 | ${pkgs.writeText "result.txt" '' 47 | before:dn-before 48 | between:dn-between 49 | after:dn-after 50 | ''} 51 | ''; 52 | }; 53 | } 54 | -------------------------------------------------------------------------------- /docs/manual/contributing.md: -------------------------------------------------------------------------------- 1 | # Contributing {#ch-contributing} 2 | 3 | Contributions to Project Manager are very welcome. To make the process as 4 | smooth as possible for both you and the Project Manager maintainers we 5 | provide some guidelines that we ask you to follow. See [Getting 6 | started](#sec-contrib-getting-started) for information on how to set up 7 | a suitable development environment and [Guidelines](#sec-guidelines) for 8 | the actual guidelines. 9 | 10 | This text is mainly directed at those who would like to make code 11 | contributions to Project Manager. If you just want to report a bug then 12 | first look among the already [open 13 | issues](https://github.com/sellout/project-manager/issues), if you 14 | find one matching yours then feel free to comment on it to add any 15 | additional information you may have. If no matching issue exists then go 16 | to the [new 17 | issue](https://github.com/sellout/project-manager/issues/new) page 18 | and write a description of your problem. Include as much information as 19 | you can, ideally also include relevant excerpts from your Project Manager 20 | configuration. 21 | 22 | ```{=include=} sections 23 | contributing/getting-started.md 24 | contributing/guidelines.md 25 | contributing/news.md 26 | contributing/tests.md 27 | ``` 28 | -------------------------------------------------------------------------------- /modules/services/flakestry.nix: -------------------------------------------------------------------------------- 1 | { 2 | config, 3 | lib, 4 | pkgs, 5 | ... 6 | }: let 7 | cfg = config.services.flakestry; 8 | in { 9 | meta.maintainers = [lib.maintainers.sellout]; 10 | 11 | options.services.flakestry = { 12 | enable = lib.mkEnableOption "Flakestry"; 13 | }; 14 | 15 | config = lib.mkIf cfg.enable { 16 | ## Written as `lines` instead of an attr set to make it easier to compare 17 | ## against https://flakestry.dev/publish. 18 | services.github.workflow."flakestry-publish.yml".text = '' 19 | name: "Publish a flake to flakestry" 20 | on: 21 | push: 22 | tags: 23 | - "v?[0-9]+.[0-9]+.[0-9]+" 24 | - "v?[0-9]+.[0-9]+" 25 | workflow_dispatch: 26 | inputs: 27 | tag: 28 | description: "The existing tag to publish" 29 | type: "string" 30 | required: true 31 | jobs: 32 | publish-flake: 33 | runs-on: ubuntu-24.04 34 | permissions: 35 | id-token: "write" 36 | contents: "read" 37 | steps: 38 | - uses: flakestry/flakestry-publish@main 39 | with: 40 | version: "''${{ inputs.tag || github.ref_name }}" 41 | ''; 42 | }; 43 | } 44 | -------------------------------------------------------------------------------- /docs/manual/usage/rollbacks.md: -------------------------------------------------------------------------------- 1 | # Rollbacks {#sec-usage-rollbacks} 2 | 3 | While the `project-manager` tool doesn’t explicitly support rollbacks at 4 | the moment it’s relatively easy to perform one manually. The steps to 5 | do so are 6 | 7 | 1. Run `project-manager generations` to determine which generation you 8 | wish to rollback to: 9 | 10 | ```shell 11 | $ project-manager generations 12 | 2018-01-04 11:56 : id 765 -> /nix/store/kahm1rxk77mnvd2l8pfvd4jkkffk5ijk-project-manager-generation 13 | 2018-01-03 10:29 : id 764 -> /nix/store/2wsmsliqr5yynqkdyjzb1y57pr5q2lsj-project-manager-generation 14 | 2018-01-01 12:21 : id 763 -> /nix/store/mv960kl9chn2lal5q8lnqdp1ygxngcd1-project-manager-generation 15 | 2017-12-29 21:03 : id 762 -> /nix/store/6c0k1r03fxckql4vgqcn9ccb616ynb94-project-manager-generation 16 | 2017-12-25 18:51 : id 761 -> /nix/store/czc5y6vi1rvnkfv83cs3rn84jarcgsgh-project-manager-generation 17 | … 18 | ``` 19 | 20 | 2. Copy the Nix store path of the generation you chose, for example, 21 | 22 | /nix/store/mv960kl9chn2lal5q8lnqdp1ygxngcd1-project-manager-generation 23 | 24 | for generation 763. 25 | 26 | 3. Run the `activate` script inside the copied store path: 27 | 28 | ```shell 29 | $ /nix/store/mv960kl9chn2lal5q8lnqdp1ygxngcd1-project-manager-generation/activate 30 | Starting Project Manager activation 31 | … 32 | ``` 33 | -------------------------------------------------------------------------------- /modules/modules.nix: -------------------------------------------------------------------------------- 1 | { 2 | files = ./files; 3 | # ./misc/dconf.nix 4 | # ./misc/debug.nix 5 | editorconfig = ./misc/editorconfig; 6 | news = ./misc/news.nix; 7 | # ./misc/specialisation.nix 8 | version = ./misc/version.nix; 9 | # ./misc/vte.nix 10 | xdg = ./misc/xdg.nix; 11 | # ./programs/bash.nix 12 | # ./programs/darcs.nix 13 | direnv = ./programs/direnv.nix; 14 | # ./programs/emacs.nix 15 | # ./programs/fish.nix 16 | # ./programs/gh.nix 17 | # ./programs/gh-dash.nix 18 | # ./programs/git-cliff.nix 19 | # ./programs/git-credential-oauth.nix 20 | git = ./programs/git; 21 | just = ./programs/just.nix; 22 | mercurial = ./programs/mercurial.nix; 23 | # ./programs/neovim.nix 24 | # ./programs/nushell.nix 25 | project-manager = ./programs/project-manager.nix; 26 | # ./programs/pylint.nix 27 | shellcheck = ./programs/shellcheck.nix; 28 | treefmt = ./programs/treefmt.nix; 29 | vale = ./programs/vale.nix; 30 | # ./programs/vim.nix 31 | # ./programs/vscode.nix 32 | # ./programs/zellij.nix 33 | # ./programs/zsh.nix 34 | project-environment = ./project-environment.nix; 35 | flakehub = ./services/flakehub; 36 | flakestry = ./services/flakestry.nix; 37 | garnix = ./services/garnix.nix; 38 | github = ./services/github.nix; 39 | # ./services/git-sync.nix 40 | # ./services/lorri.nix 41 | nix-cx = ./services/nix-ci.nix; 42 | renovate = ./services/renovate; 43 | } 44 | # // optional useNixpkgsModule ./misc/nixpkgs.nix 45 | # // optional (!useNixpkgsModule) ./misc/nixpkgs-disabled.nix 46 | 47 | -------------------------------------------------------------------------------- /docs/manual/nix-flakes/prerequisites.md: -------------------------------------------------------------------------------- 1 | # Prerequisites {#sec-flakes-prerequisites} 2 | 3 | - Install Nix 2.4 or later, or have it in `nix-shell`. 4 | 5 | - Enable experimental features `nix-command` and `flakes`. 6 | 7 | - When using NixOS, add the following to your `configuration.nix` 8 | and rebuild your system. 9 | 10 | ```nix 11 | nix = { 12 | package = pkgs.nixFlakes; 13 | extraOptions = '' 14 | experimental-features = nix-command flakes 15 | ''; 16 | }; 17 | ``` 18 | 19 | - If you aren’t using NixOS, add the following to `nix.conf` 20 | (located at `~/.config/nix/` or `/etc/nix/nix.conf`). 21 | 22 | ```bash 23 | experimental-features = nix-command flakes 24 | ``` 25 | 26 | You may need to restart the Nix daemon with, for example, 27 | `sudo systemctl restart nix-daemon.service`. 28 | 29 | - You can also enable flakes on a per-command basis with 30 | the following extra flags to `nix` and `project-manager`: 31 | 32 | ```shell 33 | $ nix --extra-experimental-features "nix-command flakes" 34 | $ project-manager --extra-experimental-features "nix-command flakes" 35 | ``` 36 | 37 | - Prepare your Project Manager configuration (`.config/project/default.nix`). 38 | 39 | Unlike the channel-based setup, `.config/project/default.nix` will be evaluated when 40 | the flake is built, so it must be present before bootstrap of Project 41 | Manager from the flake. See [Configuration Example](#sec-usage-configuration) for 42 | introduction about writing a Project Manager configuration. 43 | -------------------------------------------------------------------------------- /modules/lib/generators.nix: -------------------------------------------------------------------------------- 1 | ## This provides the generators provided by Nixpkgs with two changes: 2 | ## 3 | ## 1. when possible, it adds a “generated by Project Manager” comment and 4 | ## 2. when necessary, it adds a trailing newline, to make it a valid POSIX file. 5 | {lib}: let 6 | message = "This file was generated by Project Manager."; 7 | in { 8 | toDhall = args: v: 9 | '' 10 | --| ${message} 11 | '' 12 | + lib.generators.toDhall args v; 13 | toGitINI = v: 14 | '' 15 | ; ${message} 16 | '' 17 | + lib.generators.toGitINI v; 18 | toINI = args: v: 19 | '' 20 | ; ${message} 21 | '' 22 | + lib.generators.toINI args v; 23 | toINIWithGlobalSection = args: v: 24 | '' 25 | ; ${message} 26 | '' 27 | + lib.generators.toINIWithGlobalSection args v; 28 | ## NB: JSON doesn't support comments, but we re-export `toJSON` here for 29 | ## consistency. 30 | toJSON = args: v: lib.generators.toJSON args v + "\n"; 31 | toLua = {indent, ...} @ args: v: 32 | '' 33 | ${indent}-- ${message} 34 | '' 35 | + lib.generators.toLua args v; 36 | toPlist = args: v: 37 | '' 38 | 39 | '' 40 | + lib.generators.toPlist args v; 41 | toPretty = args: v: 42 | '' 43 | # ${message} 44 | '' 45 | + lib.generators.toPretty args v 46 | + "\n"; 47 | toTOML = args: v: 48 | '' 49 | # ${message} 50 | '' 51 | + lib.generators.toINIWithGlobalSection args v; 52 | toYAML = options: v: 53 | '' 54 | # ${message} 55 | '' 56 | + lib.generators.toYAML options v 57 | + "\n"; 58 | } 59 | -------------------------------------------------------------------------------- /docs/manual/usage/dotfiles.md: -------------------------------------------------------------------------------- 1 | # Keeping your project root safe from harm {#sec-usage-dotfiles} 2 | 3 | To configure programs and services Project Manager must write various things to your project directory. Project Manager will attempt to detect collisions with existing files, but this can be difficult. Whereas Home Manager can rely on symlinks to identify files that have been produced during activation, Project Manager can’t always do this. Files with [“repository” persistence](#opt-project.file._name_.minimum-persistence) must be either hard links or copies (if the project is on a different volume than the Nix store). Project Manager must assume these files can be overwritten. This is why it’s important to ensure that changes are stashed or committed before running `project-manager switch`. 4 | 5 | If there are collisions between existing files and files with `worktree` persistence, these will cause Project Manager to terminate before changing _any_ files. (Files with “store” persistence don’t exist in the working tree and thus can never prevent switching.) 6 | 7 | For example, suppose you have a wonderful, painstakingly created `$PROJECT_ROOT/.git/config` and add 8 | 9 | ```nix 10 | { 11 | # … 12 | 13 | programs.git = { 14 | enable = true; 15 | userName = "Jane Doe"; 16 | userEmail = "jane.doe@example.org"; 17 | }; 18 | 19 | # … 20 | } 21 | ``` 22 | 23 | to your configuration. Attempting to switch to the generation will then result in 24 | 25 | ```shell 26 | $ project-manager switch 27 | … 28 | Activating checkLinkTargets 29 | Existing file '/…/.git/config' is in the way 30 | Please move the above files and try again 31 | ``` 32 | -------------------------------------------------------------------------------- /docs/manual/contributing/getting-started.md: -------------------------------------------------------------------------------- 1 | # Getting started {#sec-contrib-getting-started} 2 | 3 | 4 | 5 | If you haven’t forked Project Manager yet, then you need to do that 6 | first. Have a look at GitHub's [Fork a 7 | repo](https://help.github.com/articles/fork-a-repo/) for instructions on 8 | how to do this. 9 | 10 | 11 | 12 | Once you have a fork of Project Manager you should create a branch starting 13 | at the most recent `master` branch. Give your branch a reasonably 14 | descriptive name. Commit your changes to this branch and when you are 15 | happy with the result and it fulfills [Guidelines](#sec-guidelines) then 16 | push the branch to GitHub and [create a pull 17 | request](https://help.github.com/articles/creating-a-pull-request/). 18 | 19 | Assuming your clone is at `$HOME/devel/project-manager` then you can make 20 | the `project-manager` command use it by either 21 | 22 | 1. overriding the default path by using the `-I` command line option: 23 | 24 | ```shell 25 | $ project-manager -I project-manager=$HOME/devel/project-manager 26 | ``` 27 | 28 | or, if using [flakes](#sec-flakes-standalone): 29 | 30 | ```shell 31 | $ project-manager --override-input project-manager ~/devel/project-manager 32 | ``` 33 | 34 | or 35 | 36 | 2. changing the default path by ensuring your configuration includes 37 | 38 | ```nix 39 | programs.project-manager.enable = true; 40 | programs.project-manager.path = "$HOME/devel/project-manager"; 41 | ``` 42 | 43 | and running `project-manager switch` to activate the change. 44 | Afterward, `project-manager build` and `project-manager switch` will use 45 | your cloned repository. 46 | 47 | The first option is good if you only temporarily want to use your clone. 48 | -------------------------------------------------------------------------------- /docs/manual/installation.md: -------------------------------------------------------------------------------- 1 | # Installing Project Manager {#ch-installation} 2 | 3 | **NB**: Project Manager doesn’t need to be installed outside of the projects it’s used in. In most projects that use Project Manager, `nix develop` (or maybe `nix develop .#project-manager`) should put you into a shell that makes `project-manager` available to you. The exception to this is when you are [adding Project Manager to a project](#ch-quick-start). 4 | 5 | That said, it can be useful to have Project Manager installed more widely. Currently this can only be done via a flake. This snippet includes examples for nix-darwin, Home Manager, and NixOS. You don’t need to include all of them. 6 | 7 | ```nix 8 | { 9 | outputs = {home-manager, nix-darwin, nixpkgs, project-manager, self ...}: let 10 | system = "x86_64-linux"; # or another system 11 | pkgs = import nixpkgs { 12 | inherit system; 13 | overlays = [project-manager.overlays.default]; 14 | }; 15 | { 16 | darwinConfigurations."host" = nix-darwin.lib.homeManagerConfiguration { 17 | inherit pkgs; 18 | modules = [ 19 | ({pkgs, ...}: { 20 | environment.systemPackages = [pkgs.project-manager]; 21 | }) 22 | ]; 23 | }; 24 | 25 | homeConfigurations."user@host" = home-manager.lib.homeManagerConfiguration { 26 | inherit pkgs; 27 | modules = [ 28 | ({pkgs, ...}: { 29 | home.packages = [pkgs.project-manager]; 30 | }) 31 | ]; 32 | }; 33 | 34 | nixosConfigurations."host" = nixpkgs.lib.nixosSystem { 35 | inherit pkgs system; 36 | modules = [ 37 | ({pkgs, ...}: { 38 | environment.systemPackages = [pkgs.project-manager]; 39 | }) 40 | ]; 41 | }; 42 | }; 43 | 44 | inputs.project-manager.url = "github:sellout/project-manager"; 45 | } 46 | ``` 47 | -------------------------------------------------------------------------------- /docs/project-manager-manual.nix: -------------------------------------------------------------------------------- 1 | { 2 | stdenv, 3 | lib, 4 | documentation-highlighter, 5 | revision, 6 | project-manager-options, 7 | nixos-render-docs, 8 | }: let 9 | outputPath = "share/doc/project-manager"; 10 | in 11 | stdenv.mkDerivation { 12 | name = "project-manager-manual"; 13 | 14 | nativeBuildInputs = [nixos-render-docs]; 15 | 16 | src = ./manual; 17 | 18 | buildPhase = '' 19 | mkdir -p out/{highlightjs,media} 20 | 21 | cp -t out/highlightjs \ 22 | ${documentation-highlighter}/highlight.pack.js \ 23 | ${documentation-highlighter}/LICENSE \ 24 | ${documentation-highlighter}/mono-blue.css \ 25 | ${documentation-highlighter}/loader.js 26 | 27 | substituteInPlace ./options.md \ 28 | --subst-var-by \ 29 | OPTIONS_JSON \ 30 | ${project-manager-options.project-manager}/share/doc/nixos/options.json 31 | 32 | cp ${./options.html} out/options.html 33 | 34 | cp ${./static/style.css} out/style.css 35 | 36 | cp -r ${./release-notes} release-notes 37 | 38 | nixos-render-docs manual html \ 39 | --manpage-urls ./manpage-urls.json \ 40 | --revision ${lib.trivial.revisionWithDefault revision} \ 41 | --stylesheet style.css \ 42 | --script highlightjs/highlight.pack.js \ 43 | --script highlightjs/loader.js \ 44 | --toc-depth 1 \ 45 | --section-toc-depth 1 \ 46 | manual.md \ 47 | out/index.xhtml 48 | ''; 49 | 50 | installPhase = '' 51 | dest="$out/${outputPath}" 52 | mkdir -p "$(dirname "$dest")" 53 | mv out "$dest" 54 | 55 | mkdir -p $out/nix-support/ 56 | echo "doc manual $dest index.html" >> $out/nix-support/hydra-build-products 57 | ''; 58 | 59 | meta = {maintainers = [lib.maintainers.considerate];}; 60 | } 61 | -------------------------------------------------------------------------------- /modules/lib/path.nix: -------------------------------------------------------------------------------- 1 | {lib}: { 2 | ## This isn't in Nixpkgs 23.05, but should be in the next release. 3 | subpath.components = path: let 4 | parts = builtins.split "/+(\\./+)*" path; 5 | partCount = lib.length parts / 2 + 1; 6 | skipStart = 7 | if lib.head parts == "." 8 | then 1 9 | else 0; 10 | skipEnd = 11 | if lib.last parts == "." || lib.last parts == "" 12 | then 1 13 | else 0; 14 | componentCount = partCount - skipEnd - skipStart; 15 | in 16 | if path == "." 17 | then [] 18 | else 19 | lib.genList ( 20 | index: 21 | lib.elemAt parts ((skipStart + index) * 2) 22 | ) 23 | componentCount; 24 | 25 | ## Find a path to `dest` from `source`. `source` must be a directory, not a 26 | ## file. 27 | ## 28 | ## TODO: This should normalize. Right now it always uses ../ up to the common 29 | ## parent then rebuilds the entire path. E.g., 30 | ## `route ./foo/bar/baz ./foo/bar/zab` is `../../../foo/bar/zab`, but it 31 | ## could be `../zab`. 32 | routeFromDir = sourceDir: destPath: 33 | lib.concatStringsSep 34 | "/" 35 | (map (_: "..") (lib.tail (lib.pm.path.subpath.components sourceDir))) 36 | + "/" 37 | + destPath; 38 | 39 | ## Find a path to `dest` from `source`. `source` must be a directory, not a 40 | ## file. 41 | ## 42 | ## TODO: This should normalize. Right now it always uses ../ up to the common 43 | ## parent then rebuilds the entire path. E.g., 44 | ## `route ./foo/bar/baz ./foo/bar/zab` is `../../../foo/bar/zab`, but it 45 | ## could be `../zab`. 46 | routeFromFile = sourceFile: destPath: 47 | lib.concatStringsSep 48 | "/" 49 | (map (_: "..") (lib.tail (lib.init (lib.pm.path.subpath.components sourceFile)))) 50 | + "/" 51 | + destPath; 52 | } 53 | -------------------------------------------------------------------------------- /modules/default.nix: -------------------------------------------------------------------------------- 1 | { 2 | configuration, 3 | modules, 4 | pkgs, 5 | lib ? pkgs.lib, 6 | # Whether to check that each option has a matching declaration. 7 | check ? true, 8 | # Extra arguments passed to specialArgs. 9 | extraSpecialArgs ? {}, 10 | }: let 11 | collectFailed = cfg: 12 | map (x: x.message) (lib.filter (x: !x.assertion) cfg.assertions); 13 | 14 | showWarnings = res: let 15 | f = w: x: builtins.trace "warning: ${w}" x; 16 | in 17 | lib.fold f res res.config.warnings; 18 | 19 | extendedLib = import ./lib/stdlib-extended.nix lib; 20 | 21 | pmModules = import ./all-modules.nix { 22 | inherit check modules pkgs; 23 | lib = extendedLib; 24 | }; 25 | 26 | rawModule = extendedLib.evalModules { 27 | modules = [configuration] ++ pmModules; 28 | specialArgs = 29 | { 30 | modulesPath = builtins.toString ./.; 31 | } 32 | // extraSpecialArgs; 33 | }; 34 | 35 | module = showWarnings ( 36 | let 37 | failed = collectFailed rawModule.config; 38 | failedStr = lib.concatStringsSep "\n" (map (x: "- ${x}") failed); 39 | in 40 | if failed == [] 41 | then rawModule 42 | else throw "\nFailed assertions:\n${failedStr}" 43 | ); 44 | in { 45 | inherit (module) options config; 46 | 47 | inherit 48 | (module.config.project) 49 | packages 50 | checks 51 | sandboxedChecks 52 | unsandboxedChecks 53 | devShells 54 | formatter 55 | cleanRepositoryPersisted 56 | cleanRepositoryPersistedExcept 57 | filterRepositoryPersisted 58 | filterRepositoryPersistedExcept 59 | ; 60 | 61 | newsDisplay = rawModule.config.news.display; 62 | newsEntries = lib.sort (a: b: a.time > b.time) ( 63 | lib.filter (a: a.condition) rawModule.config.news.entries 64 | ); 65 | 66 | inherit (module._module.args) pkgs; 67 | } 68 | -------------------------------------------------------------------------------- /project-manager/build-news.nix: -------------------------------------------------------------------------------- 1 | # Used by the project-manager tool to present news to the user. The content of 2 | # this file is considered internal and the exported fields may change without 3 | # warning. 4 | { 5 | newsJsonFile, 6 | newsReadIdsFile ? null, 7 | }: let 8 | inherit 9 | (builtins) 10 | concatStringsSep 11 | filter 12 | hasAttr 13 | isString 14 | length 15 | readFile 16 | replaceStrings 17 | sort 18 | split 19 | ; 20 | 21 | newsJson = builtins.fromJSON (builtins.readFile newsJsonFile); 22 | 23 | # Sorted and relevant entries. 24 | relevantEntries = 25 | sort (a: b: a.time > b.time) (filter (e: e.condition) newsJson.entries); 26 | 27 | newsReadIds = 28 | if newsReadIdsFile == null 29 | then {} 30 | else let 31 | ids = filter isString (split "\n" (readFile newsReadIdsFile)); 32 | in 33 | builtins.listToAttrs (map (id: { 34 | name = id; 35 | value = null; 36 | }) 37 | ids); 38 | 39 | newsIsRead = entry: hasAttr entry.id newsReadIds; 40 | 41 | newsUnread = let 42 | pred = entry: entry.condition && !newsIsRead entry; 43 | in 44 | filter pred relevantEntries; 45 | 46 | prettyTime = t: replaceStrings ["T" "+00:00"] [" " ""] t; 47 | 48 | layoutNews = entries: let 49 | mkTextEntry = entry: let 50 | flag = 51 | if newsIsRead entry 52 | then "read" 53 | else "unread"; 54 | in '' 55 | * ${prettyTime entry.time} [${flag}] 56 | 57 | ${replaceStrings ["\n"] ["\n "] entry.message} 58 | ''; 59 | in 60 | concatStringsSep "\n\n" (map mkTextEntry entries); 61 | in { 62 | meta = { 63 | numUnread = length newsUnread; 64 | display = newsJson.display; 65 | ids = concatStringsSep "\n" (map (e: e.id) newsJson.entries); 66 | }; 67 | news = { 68 | all = layoutNews relevantEntries; 69 | unread = layoutNews newsUnread; 70 | }; 71 | } 72 | -------------------------------------------------------------------------------- /modules/programs/treefmt.nix: -------------------------------------------------------------------------------- 1 | { 2 | config, 3 | lib, 4 | pkgs, 5 | pmPkgs, 6 | self, 7 | treefmt-nix, 8 | ... 9 | }: let 10 | cfg = config.programs.treefmt; 11 | in { 12 | meta.maintainers = [lib.maintainers.sellout]; 13 | 14 | options.programs.treefmt = { 15 | enable = lib.mkEnableOption "treefmt"; 16 | 17 | package = lib.mkPackageOption pkgs "treefmt" {}; 18 | 19 | projectRootFile = lib.mkOption { 20 | type = lib.types.str; 21 | default = "flake.nix"; 22 | description = '' 23 | The file to use to identify the root of the project for formatting. 24 | ''; 25 | }; 26 | 27 | programs = lib.mkOption { 28 | type = lib.types.attrsOf lib.types.attrs; 29 | default = {}; 30 | description = '' 31 | Configuration for treefmt formatters. See 32 | https://github.com/numtide/treefmt-nix#configuration for details. 33 | ''; 34 | }; 35 | 36 | settings = lib.mkOption { 37 | type = lib.types.attrsOf lib.types.attrs; 38 | default = {}; 39 | description = '' 40 | Settings for treefmt. See 41 | https://github.com/numtide/treefmt-nix#configuration for details. 42 | ''; 43 | }; 44 | }; 45 | 46 | config = lib.mkIf cfg.enable (let 47 | newExcludes = 48 | lib.mapAttrsToList (k: v: v.target) 49 | (lib.filterAttrs (k: v: v.persistence == "repository") 50 | config.project.file); 51 | format = treefmt-nix.lib.evalModule pmPkgs { 52 | inherit (cfg) package projectRootFile programs; 53 | settings = 54 | cfg.settings 55 | // { 56 | global.excludes = 57 | (cfg.settings.global.excludes or []) ++ newExcludes; 58 | }; 59 | }; 60 | in { 61 | project = { 62 | checks.formatter = format.config.build.check self; 63 | formatter = format.config.build.wrapper; 64 | devPackages = [format.config.build.wrapper]; 65 | }; 66 | }); 67 | } 68 | -------------------------------------------------------------------------------- /docs/manual/faq/change-package-module.md: -------------------------------------------------------------------------------- 1 | 2 | # How do I change the package used by a module? {#_how_do_i_change_the_package_used_by_a_module} 3 | 4 | By default Project Manager will install the package provided by your chosen 5 | `nixpkgs` channel but occasionally you might end up needing to change 6 | this package. This can typically be done in two ways. 7 | 8 | 1. If the module provides a `package` option, such as 9 | `programs.beets.package`, then this is the recommended way to 10 | perform the change. For example, 11 | 12 | ```nix 13 | programs.beets.package = pkgs.beets.override { pluginOverrides = { beatport.enable = false; }; }; 14 | ``` 15 | 16 | See [Nix pill 17](https://nixos.org/guides/nix-pills/nixpkgs-overriding-packages.html) 17 | for more information on package overrides. Alternatively, if you want 18 | to use the `beets` package from Nixpkgs unstable, then a configuration like 19 | 20 | ```nix 21 | { pkgs, config, ... }: 22 | 23 | let 24 | 25 | pkgsUnstable = import {}; 26 | 27 | in 28 | 29 | { 30 | programs.beets.package = pkgsUnstable.beets; 31 | 32 | # … 33 | } 34 | ``` 35 | 36 | should work OK. 37 | 38 | 2. If no `package` option is available then you can typically change 39 | the relevant package using an 40 | [overlay](https://nixos.org/nixpkgs/manual/#chap-overlays). 41 | 42 | For example, if you want to use the `programs.skim` module but use 43 | the `skim` package from Nixpkgs unstable, then a configuration like 44 | 45 | ```nix 46 | { pkgs, config, ... }: 47 | 48 | let 49 | 50 | pkgsUnstable = import {}; 51 | 52 | in 53 | 54 | { 55 | programs.skim.enable = true; 56 | 57 | nixpkgs.overlays = [ 58 | (self: super: { 59 | skim = pkgsUnstable.skim; 60 | }) 61 | ]; 62 | 63 | # … 64 | } 65 | ``` 66 | 67 | should work OK. 68 | -------------------------------------------------------------------------------- /docs/manual/nix-flakes/standalone.md: -------------------------------------------------------------------------------- 1 | # Standalone setup {#sec-flakes-standalone} 2 | 3 | To prepare an initial Project Manager configuration for your logged in 4 | user, you can run the Project Manager `init` command directly from its 5 | flake. 6 | 7 | For example, to generate and activate a basic configuration run the command 8 | 9 | ```shell 10 | $ nix run github:sellout/project-manager -- init --switch 11 | ``` 12 | 13 | This will generate a `flake.nix` and a `default.nix` file in 14 | `./.config/project`, creating the directory if it doesn’t exist. 15 | 16 | If you omit the `--switch` option then the activation won’t happen. 17 | This is useful if you want to inspect and edit the configuration before 18 | activating it. 19 | 20 | ```shell 21 | $ nix run github:sellout/project-manager -- init 22 | $ # Edit files in ~/.config/project-manager 23 | $ nix run github:sellout/project-manager -- init --switch 24 | ``` 25 | 26 | After the initial activation has completed successfully then building 27 | and activating your flake-based configuration is as simple as 28 | 29 | ```shell 30 | $ project-manager switch 31 | ``` 32 | 33 | It’s possible to override the default configuration directory, if you 34 | want. For example, 35 | 36 | ```shell 37 | $ nix run github:sellout/project-manager -- init --switch ~/hmconf 38 | $ # And after the initial activation. 39 | $ project-manager switch --flake ~/hmconf 40 | ``` 41 | 42 | ::: {.note} 43 | The flake inputs aren’t automatically updated by Project Manager. You need 44 | to use the standard `nix flake update` command for that. 45 | 46 | If you only want to update a single flake input, then the command 47 | `nix flake lock --update-input ` can be used. 48 | 49 | You can also pass flake-related options such as `--recreate-lock-file` 50 | or `--update-input ` to `project-manager` when building or 51 | switching, and these options will be forwarded to `nix build`. See the 52 | [NixOS Wiki page](https://wiki.nixos.org/wiki/Flakes) for details. 53 | ::: 54 | -------------------------------------------------------------------------------- /modules/misc/editorconfig/default.nix: -------------------------------------------------------------------------------- 1 | ### This configures basic cross-editor formatting. 2 | ### 3 | ### See https://editorconfig.org/ for more info, and to see if your editor 4 | ### requires a plugin to take advantage of it. 5 | { 6 | config, 7 | lib, 8 | pkgs, 9 | ... 10 | }: 11 | with lib; let 12 | cfg = config.editorconfig; 13 | 14 | iniFormat = pkgs.formats.ini {}; 15 | in { 16 | meta.maintainers = with maintainers; [sellout]; 17 | 18 | options.editorconfig = { 19 | enable = 20 | mkEnableOption "EditorConfig project configuration file"; 21 | 22 | settings = mkOption { 23 | type = iniFormat.type; 24 | default = {}; 25 | description = '' 26 | Configuration written to {file}`$PROJECT_ROOT/.editorconfig`. 27 | `root = true` is automatically added to the file, 28 | it must not be added here. 29 | See for documentation. 30 | ''; 31 | example = lib.literalMD '' 32 | { 33 | "*" = { 34 | charset = "utf-8"; 35 | end_of_line = "lf"; 36 | trim_trailing_whitespace = true; 37 | insert_final_newline = true; 38 | max_line_width = 78; 39 | indent_style = "space"; 40 | indent_size = 4; 41 | }; 42 | }; 43 | ''; 44 | }; 45 | }; 46 | 47 | config = mkIf (cfg.enable && cfg.settings != {}) { 48 | programs.vale.excludes = mkIf (config.project.file.".editorconfig".persistence != "store") [ 49 | "./${config.project.file.".editorconfig".target}" 50 | ]; 51 | 52 | project.file.".editorconfig" = { 53 | ## TODO: This needs to be committed so that it affects the format `check`. 54 | ## However, it should really be linked into there from the store 55 | ## instead, and this should be “worktree” so that editors can find 56 | ## it. 57 | minimum-persistence = "repository"; 58 | text = lib.pm.generators.toTOML {} { 59 | globalSection = {root = true;}; 60 | sections = cfg.settings; 61 | }; 62 | }; 63 | }; 64 | } 65 | -------------------------------------------------------------------------------- /docs/static/tomorrow-night.min.css: -------------------------------------------------------------------------------- 1 | /*! 2 | Theme: Tomorrow Night 3 | Author: Chris Kempson (http://chriskempson.com) 4 | License: ~ MIT (or more permissive) [via base16-schemes-source] 5 | Maintainer: @highlightjs/core-team 6 | Version: 2021.09.0 7 | */ 8 | pre code.hljs { 9 | display: block; 10 | overflow-x: auto; 11 | padding: 1em; 12 | } 13 | code.hljs { 14 | padding: 3px 5px; 15 | } 16 | .hljs { 17 | color: #ccc; 18 | background: #2d2d2d; 19 | } 20 | .hljs ::selection, 21 | .hljs::selection { 22 | background-color: #515151; 23 | color: #ccc; 24 | } 25 | .hljs-comment { 26 | color: #999; 27 | } 28 | .hljs-tag { 29 | color: #b4b7b4; 30 | } 31 | .hljs-operator, 32 | .hljs-punctuation, 33 | .hljs-subst { 34 | color: #ccc; 35 | } 36 | .hljs-operator { 37 | opacity: 0.7; 38 | } 39 | .hljs-bullet, 40 | .hljs-deletion, 41 | .hljs-name, 42 | .hljs-selector-tag, 43 | .hljs-template-variable, 44 | .hljs-variable { 45 | color: #f2777a; 46 | } 47 | .hljs-attr, 48 | .hljs-link, 49 | .hljs-literal, 50 | .hljs-number, 51 | .hljs-symbol, 52 | .hljs-variable.constant_ { 53 | color: #f99157; 54 | } 55 | .hljs-class .hljs-title, 56 | .hljs-title, 57 | .hljs-title.class_ { 58 | color: #fc6; 59 | } 60 | .hljs-strong { 61 | font-weight: 700; 62 | color: #fc6; 63 | } 64 | .hljs-addition, 65 | .hljs-code, 66 | .hljs-string, 67 | .hljs-title.class_.inherited__ { 68 | color: #9c9; 69 | } 70 | .hljs-built_in, 71 | .hljs-doctag, 72 | .hljs-keyword.hljs-atrule, 73 | .hljs-quote, 74 | .hljs-regexp { 75 | color: #6cc; 76 | } 77 | .hljs-attribute, 78 | .hljs-function .hljs-title, 79 | .hljs-section, 80 | .hljs-title.function_, 81 | .ruby .hljs-property { 82 | color: #69c; 83 | } 84 | .diff .hljs-meta, 85 | .hljs-keyword, 86 | .hljs-template-tag, 87 | .hljs-type { 88 | color: #c9c; 89 | } 90 | .hljs-emphasis { 91 | color: #c9c; 92 | font-style: italic; 93 | } 94 | .hljs-meta, 95 | .hljs-meta .hljs-keyword, 96 | .hljs-meta .hljs-string { 97 | color: #a3685a; 98 | } 99 | .hljs-meta .hljs-keyword, 100 | .hljs-meta-keyword { 101 | font-weight: 700; 102 | } 103 | -------------------------------------------------------------------------------- /docs/manual/faq/collision.md: -------------------------------------------------------------------------------- 1 | 2 | # Why is there a collision error when switching generation? {#_why_is_there_a_collision_error_when_switching_generation} 3 | 4 | Project Manager currently installs packages into the user environment, 5 | precisely as if the packages were installed through `nix-env --install`. 6 | This means that you will get a collision error if your Project Manager 7 | configuration attempts to install a package that you already have 8 | installed manually, that is, packages that shows up when you run 9 | `nix-env --query`. 10 | 11 | For example, imagine you have the `hello` package installed in your 12 | environment 13 | 14 | ```shell 15 | $ nix-env --query 16 | hello-2.10 17 | ``` 18 | 19 | and your Project Manager configuration contains 20 | 21 | ```nix 22 | project.devPackages = [ pkgs.hello ]; 23 | ``` 24 | 25 | Then attempting to switch to this configuration will result in an error 26 | similar to 27 | 28 | ```shell 29 | $ project-manager switch 30 | these derivations will be built: 31 | /nix/store/xg69wsnd1rp8xgs9qfsjal017nf0ldhm-project-manager-path.drv 32 | […] 33 | Activating installPackages 34 | replacing old ‘project-manager-path’ 35 | installing ‘project-manager-path’ 36 | building path(s) ‘/nix/store/b5c0asjz9f06l52l9812w6k39ifr49jj-project-environment’ 37 | Wide character in die at /nix/store/64jc9gd2rkbgdb4yjx3nrgc91bpjj5ky-buildenv.pl line 79. 38 | collision between ‘/nix/store/fmwa4axzghz11cnln5absh31nbhs9lq1-project-manager-path/bin/hello’ and ‘/nix/store/c2wyl8b9p4afivpcz8jplc9kis8rj36d-hello-2.10/bin/hello’; use ‘nix-env --set-flag priority NUMBER PKGNAME’ to change the priority of one of the conflicting packages 39 | builder for ‘/nix/store/b37x3s7pzxbasfqhaca5dqbf3pjjw0ip-project-environment.drv’ failed with exit code 2 40 | error: build of ‘/nix/store/b37x3s7pzxbasfqhaca5dqbf3pjjw0ip-project-environment.drv’ failed 41 | ``` 42 | 43 | The solution is typically to uninstall the package from the environment 44 | using `nix-env --uninstall` and reattempt the Project Manager generation 45 | switch. 46 | 47 | You could also opt to uninstall _all_ of the packages from your profile 48 | with `nix-env --uninstall '*'`. 49 | -------------------------------------------------------------------------------- /modules/programs/project-manager.nix: -------------------------------------------------------------------------------- 1 | { 2 | config, 3 | lib, 4 | pkgs, 5 | project-manager, 6 | ... 7 | }: 8 | with lib; let 9 | cfg = config.programs.project-manager; 10 | in { 11 | meta.maintainers = [maintainers.sellout]; 12 | 13 | options = { 14 | programs.project-manager = { 15 | enable = mkEnableOption "Project Manager"; 16 | 17 | package = mkOption { 18 | type = types.package; 19 | default = project-manager.packages.${pkgs.system}.project-manager; 20 | defaultText = "the instance used to build this configuration"; 21 | description = '' 22 | The current Package Manager package. 23 | ''; 24 | }; 25 | 26 | path = mkOption { 27 | type = types.nullOr types.str; 28 | default = null; 29 | example = "$PROJECT_ROOT/devel/project-manager"; 30 | description = '' 31 | The default path to use for Project Manager. When 32 | `null`, {file}`$PROJECT_ROOT/.config/nixpkgs/project-manager` 33 | will be attempted. 34 | ''; 35 | }; 36 | 37 | automaticallyExpireGenerations = mkOption { 38 | type = types.nullOr types.str; 39 | default = "now"; 40 | example = "-30 days"; 41 | description = '' 42 | If this is non-`null`, Project Manager will remove links to old 43 | generations during activation. This is akin to running 44 | `project-manager expire-generations {var}duration`. The default value 45 | is intended to leave no extra generations around, but a short value 46 | would allow for rollbacks. Be careful, though, as generations 47 | introduce GC roots, so it could result in a much larger Nix store. 48 | ''; 49 | }; 50 | }; 51 | }; 52 | 53 | config = mkIf cfg.enable { 54 | project.devPackages = [cfg.package]; 55 | 56 | project.activation.automaticallyExpireGenerations = 57 | mkIf (cfg.automaticallyExpireGenerations != null) 58 | (pm.dag.entryAfter ["linkGeneration"] '' 59 | pm_expireGenerations ${lib.escapeShellArg cfg.automaticallyExpireGenerations} 60 | ''); 61 | }; 62 | } 63 | -------------------------------------------------------------------------------- /modules/lib/types/dag.nix: -------------------------------------------------------------------------------- 1 | {lib}: let 2 | inherit (lib) defaultFunctor pm mkIf mkOrder mkOption mkOptionType types; 3 | 4 | dagEntryOf = elemType: let 5 | submoduleType = types.submodule ({name, ...}: { 6 | options = { 7 | data = mkOption {type = elemType;}; 8 | after = mkOption {type = with types; listOf str;}; 9 | before = mkOption {type = with types; listOf str;}; 10 | }; 11 | config = mkIf (elemType.name == "submodule") { 12 | data._module.args.dagName = name; 13 | }; 14 | }); 15 | maybeConvert = def: 16 | if pm.dag.isEntry def.value 17 | then def.value 18 | else 19 | pm.dag.entryAnywhere ( 20 | if def ? priority 21 | then mkOrder def.priority def.value 22 | else def.value 23 | ); 24 | in 25 | mkOptionType { 26 | name = "dagEntryOf"; 27 | description = "DAG entry of ${elemType.description}"; 28 | # leave the checking to the submodule type 29 | merge = loc: defs: 30 | submoduleType.merge loc (map (def: { 31 | inherit (def) file; 32 | value = maybeConvert def; 33 | }) 34 | defs); 35 | }; 36 | in rec { 37 | # A directed acyclic graph of some inner type. 38 | # 39 | # Note, if the element type is a submodule then the `name` argument 40 | # will always be set to the string "data" since it picks up the 41 | # internal structure of the DAG values. To give access to the 42 | # "actual" attribute name a new submodule argument is provided with 43 | # the name `dagName`. 44 | dagOf = elemType: let 45 | attrEquivalent = types.attrsOf (dagEntryOf elemType); 46 | in 47 | mkOptionType rec { 48 | name = "dagOf"; 49 | description = "DAG of ${elemType.description}"; 50 | inherit (attrEquivalent) check merge emptyValue; 51 | getSubOptions = prefix: elemType.getSubOptions (prefix ++ [""]); 52 | getSubModules = elemType.getSubModules; 53 | substSubModules = m: dagOf (elemType.substSubModules m); 54 | functor = (defaultFunctor name) // {wrapped = elemType;}; 55 | nestedTypes.elemType = elemType; 56 | }; 57 | } 58 | -------------------------------------------------------------------------------- /docs/static/tomorrow.min.css: -------------------------------------------------------------------------------- 1 | /*! 2 | Theme: Tomorrow 3 | Author: Chris Kempson (http://chriskempson.com) 4 | License: ~ MIT (or more permissive) [via base16-schemes-source] 5 | Maintainer: @highlightjs/core-team 6 | Version: 2021.09.0 7 | */ 8 | pre code.hljs { 9 | display: block; 10 | overflow-x: auto; 11 | padding: 1em; 12 | } 13 | code.hljs { 14 | padding: 3px 5px; 15 | } 16 | .hljs { 17 | color: #4d4d4c; 18 | background: #fff; 19 | } 20 | .hljs ::selection, 21 | .hljs::selection { 22 | background-color: #d6d6d6; 23 | color: #4d4d4c; 24 | } 25 | .hljs-comment { 26 | color: #8e908c; 27 | } 28 | .hljs-tag { 29 | color: #969896; 30 | } 31 | .hljs-operator, 32 | .hljs-punctuation, 33 | .hljs-subst { 34 | color: #4d4d4c; 35 | } 36 | .hljs-operator { 37 | opacity: 0.7; 38 | } 39 | .hljs-bullet, 40 | .hljs-deletion, 41 | .hljs-name, 42 | .hljs-selector-tag, 43 | .hljs-template-variable, 44 | .hljs-variable { 45 | color: #c82829; 46 | } 47 | .hljs-attr, 48 | .hljs-link, 49 | .hljs-literal, 50 | .hljs-number, 51 | .hljs-symbol, 52 | .hljs-variable.constant_ { 53 | color: #f5871f; 54 | } 55 | .hljs-class .hljs-title, 56 | .hljs-title, 57 | .hljs-title.class_ { 58 | color: #eab700; 59 | } 60 | .hljs-strong { 61 | font-weight: 700; 62 | color: #eab700; 63 | } 64 | .hljs-addition, 65 | .hljs-code, 66 | .hljs-string, 67 | .hljs-title.class_.inherited__ { 68 | color: #718c00; 69 | } 70 | .hljs-built_in, 71 | .hljs-doctag, 72 | .hljs-keyword.hljs-atrule, 73 | .hljs-quote, 74 | .hljs-regexp { 75 | color: #3e999f; 76 | } 77 | .hljs-attribute, 78 | .hljs-function .hljs-title, 79 | .hljs-section, 80 | .hljs-title.function_, 81 | .ruby .hljs-property { 82 | color: #4271ae; 83 | } 84 | .diff .hljs-meta, 85 | .hljs-keyword, 86 | .hljs-template-tag, 87 | .hljs-type { 88 | color: #8959a8; 89 | } 90 | .hljs-emphasis { 91 | color: #8959a8; 92 | font-style: italic; 93 | } 94 | .hljs-meta, 95 | .hljs-meta .hljs-keyword, 96 | .hljs-meta .hljs-string { 97 | color: #a3685a; 98 | } 99 | .hljs-meta .hljs-keyword, 100 | .hljs-meta-keyword { 101 | font-weight: 700; 102 | } 103 | -------------------------------------------------------------------------------- /modules/lib/types/tests/dag-merge.nix: -------------------------------------------------------------------------------- 1 | { 2 | config, 3 | lib, 4 | ... 5 | }: let 6 | inherit 7 | (lib) 8 | concatStringsSep 9 | pm 10 | mkIf 11 | mkMerge 12 | mkBefore 13 | mkAfter 14 | mkOption 15 | types 16 | ; 17 | 18 | dag = lib.pm.dag; 19 | 20 | result = let 21 | sorted = dag.topoSort config.tested.dag; 22 | data = map (e: "${e.name}:${e.data}") sorted.result; 23 | in 24 | concatStringsSep "\n" data + "\n"; 25 | in { 26 | options.tested.dag = mkOption {type = pm.types.dagOf types.commas;}; 27 | 28 | config = { 29 | tested.dag = mkMerge [ 30 | (mkIf false {never = "never";}) 31 | {never2 = mkIf false "never2";} 32 | { 33 | after = mkMerge [ 34 | "after" 35 | (mkIf false "neither") 36 | ]; 37 | } 38 | {before = dag.entryBefore ["after"] (mkIf true "before");} 39 | { 40 | between = mkIf true (dag.entryBetween ["after"] ["before"] "between"); 41 | } 42 | {merged = dag.entryBefore ["between"] "middle";} 43 | {merged = mkBefore "left";} 44 | {merged = dag.entryBetween ["after"] ["before"] (mkAfter "right");} 45 | { 46 | merged = dag.entryBefore ["between"] "middle"; 47 | } 48 | 49 | # Some tests of list entries. 50 | (dag.entriesAnywhere "list-anywhere" [ 51 | "list-anywhere-0" 52 | "list-anywhere-1" 53 | "list-anywhere-2" 54 | ]) 55 | {inside-list = dag.entryAfter ["list-anywhere-1"] "inside-list";} 56 | ( 57 | dag.entriesBefore "list-before" 58 | ["list-anywhere-1"] 59 | [ 60 | "list-before-0" 61 | "list-before-1" 62 | ] 63 | ) 64 | ( 65 | dag.entriesAfter "list-after" 66 | ["list-before-0"] 67 | [ 68 | "list-after-0" 69 | "list-after-1" 70 | ] 71 | ) 72 | (dag.entriesAnywhere "list-empty" []) 73 | {"list-before-0" = mkAfter "sneaky-merge";} 74 | ]; 75 | 76 | project.file."result.txt".text = result; 77 | 78 | nmt.script = '' 79 | assertFileContent \ 80 | project-files/result.txt \ 81 | ${./dag-merge-result.txt} 82 | ''; 83 | }; 84 | } 85 | -------------------------------------------------------------------------------- /docs/manual/introduction.md: -------------------------------------------------------------------------------- 1 | # Introduction to Project Manager {#ch-introduction} 2 | 3 | Project Manager is a [Nix](https://nix.dev/)-powered tool for reproducible management of the contents of project directories. 4 | This includes programs, configuration files, environment variables and, well… arbitrary files. 5 | The following example snippet of Nix code: 6 | 7 | ```nix 8 | programs.git = { 9 | enable = true; 10 | userEmail = "joe@example.org"; 11 | userName = "joe"; 12 | }; 13 | ``` 14 | 15 | would make available to a user the `git` executable and man pages and a configuration file `~/.config/git/config`: 16 | 17 | ```ini 18 | [user] 19 | email = "joe@example.org" 20 | name = "joe" 21 | ``` 22 | 23 | Since Project Manager is implemented in Nix, it provides several benefits: 24 | 25 | - Contents are reproducible — a project will be the exact same every time it’s built, unless of course, an intentional change is made. 26 | This also means you can have the exact same project on different hosts. 27 | - Significantly faster and more powerful than various backup strategies. 28 | - Unlike "dot files" repositories, Project Manager supports specifying programs, as well as their configurations. 29 | - Supported by , so that you don't have to build from source. 30 | - If you do want to build some programs from source, there is hardly a tool more useful than Nix for that, and the build instructions can be neatly integrated in your Project Manager usage. 31 | - Infinitely composable, so that values in different configuration files and build instructions can share a source of truth. 32 | - Connects you with the [most extensive](https://repology.org/repositories/statistics/total) and [most up-to-date](https://repology.org/repositories/statistics/newest) software package repository on earth, [Nixpkgs](https://github.com/NixOS/nixpkgs). 33 | 34 | Project Manager produces two things 35 | 36 | 1. Nix expressions to be used in flakes and 37 | 2. files in your project’s working tree. 38 | 39 | Either of these can be used individually, but are designed to work together. 40 | 41 | The former are accessed via `self.projectConfigurations.${system}` in your flake, while the latter are created by running `project-manager switch`. 42 | 43 | ```{=include=} sections 44 | introduction/devShell.md 45 | ``` 46 | -------------------------------------------------------------------------------- /modules/programs/just.nix: -------------------------------------------------------------------------------- 1 | { 2 | config, 3 | lib, 4 | pkgs, 5 | ... 6 | }: let 7 | cfg = config.programs.just; 8 | 9 | projectDirectory = config.project.projectDirectory; 10 | 11 | fileContents = 12 | (import ../lib/file-type.nix { 13 | inherit projectDirectory lib pkgs; 14 | commit-by-default = config.project.commit-by-default; 15 | }) 16 | .fileContents; 17 | in { 18 | meta.maintainers = [lib.maintainers.sellout]; 19 | 20 | options.programs.just = { 21 | enable = lib.mkEnableOption "[just](https://github.com/casey/just)"; 22 | 23 | package = lib.mkPackageOption pkgs "just" {}; 24 | 25 | wrapProgram = lib.mkOption { 26 | type = lib.types.nullOr lib.types.bool; 27 | default = null; 28 | apply = p: 29 | if p == null 30 | then config.project.wrapPrograms 31 | else p; 32 | 33 | description = '' 34 | Whether to wrap the executable to work without a config file in the 35 | worktree or to produce the config file. If null, this falls back to 36 | {var}`config.project.wrapPrograms`. 37 | ''; 38 | }; 39 | 40 | recipes = lib.mkOption { 41 | type = fileContents "programs.just" "" projectDirectory "recipes"; 42 | description = '' 43 | The justfile. 44 | ''; 45 | }; 46 | }; 47 | 48 | config = lib.mkIf cfg.enable (let 49 | wrapped = 50 | if cfg.wrapProgram 51 | then let 52 | flags = [ 53 | "--justfile '${config.project.file."justfile".storePath}'" 54 | "--working-directory '${config.project.projectDirectory}'" 55 | ]; 56 | in 57 | cfg.package.overrideAttrs (old: { 58 | nativeBuildInputs = old.nativeBuildInputs ++ [pkgs.makeWrapper]; 59 | 60 | postInstall = 61 | old.postInstall 62 | + '' 63 | wrapProgram "$out/bin/just" \ 64 | --add-flags "${lib.concatStringsSep " " flags}" 65 | ''; 66 | }) 67 | else cfg.package; 68 | in { 69 | project = { 70 | file."justfile" = 71 | cfg.recipes 72 | // { 73 | minimum-persistence = lib.mkIf cfg.wrapProgram "store"; 74 | target = "justfile"; 75 | }; 76 | 77 | packages = [wrapped]; 78 | }; 79 | }); 80 | } 81 | -------------------------------------------------------------------------------- /testing/asserts.nix: -------------------------------------------------------------------------------- 1 | { 2 | config, 3 | lib, 4 | ... 5 | }: let 6 | inherit (lib) concatStringsSep mkOption types; 7 | in { 8 | options.test.asserts = { 9 | warnings = { 10 | enable = mkOption { 11 | type = types.bool; 12 | default = true; 13 | description = "Whether warning asserts are enabled."; 14 | }; 15 | 16 | expected = mkOption { 17 | type = types.listOf types.str; 18 | default = []; 19 | description = '' 20 | List of expected warnings. 21 | ''; 22 | }; 23 | }; 24 | 25 | assertions = { 26 | enable = mkOption { 27 | type = types.bool; 28 | default = true; 29 | description = "Whether assertion asserts are enabled."; 30 | }; 31 | 32 | expected = mkOption { 33 | type = types.listOf types.str; 34 | default = []; 35 | description = '' 36 | List of expected assertions. 37 | ''; 38 | }; 39 | }; 40 | }; 41 | 42 | config = lib.mkMerge [ 43 | (lib.mkIf config.test.asserts.warnings.enable { 44 | project.file = { 45 | "asserts/warnings.actual".text = 46 | concatStringsSep '' 47 | 48 | -- 49 | '' 50 | config.warnings; 51 | "asserts/warnings.expected".text = 52 | concatStringsSep '' 53 | 54 | -- 55 | '' 56 | config.test.asserts.warnings.expected; 57 | }; 58 | 59 | nmt.script = '' 60 | assertFileContent \ 61 | project-files/asserts/warnings.actual \ 62 | "$TESTED/project-files/asserts/warnings.expected" 63 | ''; 64 | }) 65 | 66 | (lib.mkIf config.test.asserts.assertions.enable { 67 | project.file = { 68 | "asserts/assertions.actual".text = concatStringsSep '' 69 | 70 | -- 71 | '' (map (x: x.message) (lib.filter (x: !x.assertion) config.assertions)); 72 | "asserts/assertions.expected".text = 73 | concatStringsSep '' 74 | 75 | -- 76 | '' 77 | config.test.asserts.assertions.expected; 78 | }; 79 | 80 | nmt.script = '' 81 | assertFileContent \ 82 | project-files/asserts/assertions.actual \ 83 | "$TESTED/project-files/asserts/assertions.expected" 84 | ''; 85 | }) 86 | ]; 87 | } 88 | -------------------------------------------------------------------------------- /docs/manual/usage.md: -------------------------------------------------------------------------------- 1 | # Using Project Manager {#ch-usage} 2 | 3 | Your use of Project Manager is centered around the configuration file, 4 | which is typically found at `$PROJECT_ROOT/.config/project/default.nix` in the 5 | standard installation or `$PROJECT_ROOT/flake.nix` in a Nix 6 | flake based installation. 7 | 8 | This configuration file can be _built_ and _activated_. 9 | 10 | Building a configuration produces a directory in the Nix store that 11 | has all files and programs that should be available in your project 12 | directory and Nix project profile, respectively. The build step also checks 13 | that the configuration is valid and it will fail with an error if you, 14 | for example, assign a value to an option that doesn’t exist or assign a 15 | value of the wrong type. Some modules also have custom assertions that 16 | perform more detailed, module specific, checks. 17 | 18 | Concretely, if your configuration has 19 | 20 | ```nix 21 | programs.git.enable = "yes"; 22 | ``` 23 | 24 | then building it, for example using `project-manager build`, will result in 25 | an error message saying something like 26 | 27 | ```console 28 | $ project-manager build 29 | error: A definition for option `programs.git.enable' is not of type `boolean'. Definition values: 30 | - In `/.../.config/project/default.nix': "yes" 31 | (use '--show-trace' to show detailed location information) 32 | ``` 33 | 34 | The message indicates that you must give a Boolean value for this 35 | option, that is, either `true` or `false`. The documentation of each 36 | option will state the expected type, for 37 | [programs.git.enable](#opt-programs.git.enable) you will see "Type: boolean". You 38 | there also find information about the default value and a description of 39 | the option. You can find the complete option documentation in 40 | [Project Manager Configuration Options](#ch-options) or directly in the terminal by running 41 | 42 | ```shell 43 | man project-configuration.nix 44 | ``` 45 | 46 | Once a configuration is successfully built, it can be activated. The 47 | activation performs the steps necessary to make the files, programs, and 48 | services available in your user environment. The `project-manager switch` 49 | command performs a combined build and activation. 50 | 51 | ```{=include=} sections 52 | usage/configuration.md 53 | usage/rollbacks.md 54 | usage/dotfiles.md 55 | usage/updating.md 56 | ``` 57 | -------------------------------------------------------------------------------- /docs/manual/contributing/news.md: -------------------------------------------------------------------------------- 1 | # News {#sec-news} 2 | 3 | Project Manager includes a system for presenting news to the user. When 4 | making a change you, therefore, have the option to also include an 5 | associated news entry. In general, a news entry should only be added for 6 | truly noteworthy news. For example, a bug fix or new option doesn’t need a news entry. 7 | 8 | If you do have a change worthy of a news entry then please add one in 9 | [`news.nix`](https://github.com/sellout/project-manager/blob/main/modules/misc/news.nix) 10 | but you should follow some basic guidelines: 11 | 12 | 13 | 14 | - The entry timestamp should be in ISO-8601 format having \"+00:00\" 15 | as time zone. For example, \"2017-09-13T17:10:14+00:00\". A suitable 16 | timestamp can be produced by the command 17 | 18 | ```shell 19 | $ date --iso-8601=second --universal 20 | ``` 21 | 22 | 23 | 24 | - The entry condition should be as specific as possible. For example, 25 | if you are changing or deprecating a specific option then you could 26 | restrict the news to those users who actually use this option. 27 | 28 | - Wrap the news message so that it will fit in the typical terminal, 29 | that is, at most 80 characters wide. Ideally a bit less. 30 | 31 | - Unlike commit messages, news will be read without any connection to 32 | the Project Manager source code. It’s therefore important to make the 33 | message understandable in isolation and to those who don’t have 34 | knowledge of the Project Manager internals. To this end it should be 35 | written in more descriptive, prose like way. 36 | 37 | - If you refer to an option then write its full attribute path. That 38 | is, instead of writing 39 | 40 | The option 'foo' has been deprecated, please use 'bar' instead. 41 | 42 | it should read 43 | 44 | The option 'services.myservice.foo' has been deprecated, please 45 | use 'services.myservice.bar' instead. 46 | 47 | - A new module, say `foo.nix`, should always include a news entry that 48 | has a message similar to 49 | 50 | A new module is available: 'services.foo'. 51 | 52 | If the module is platform specific, for example, a service module using 53 | systemd, then a condition like 54 | 55 | ```nix 56 | condition = hostPlatform.isLinux; 57 | ``` 58 | 59 | should be added. If you contribute a module then you don't need to 60 | add this entry, the merger will create an entry for you. 61 | -------------------------------------------------------------------------------- /project-manager/default.nix: -------------------------------------------------------------------------------- 1 | { 2 | runStrictCommand, 3 | lib, 4 | bash-strict-mode, 5 | callPackage, 6 | coreutils, 7 | findutils, 8 | gettext, 9 | gnused, 10 | jq, 11 | less, 12 | ncurses, 13 | nix, 14 | release, 15 | unixtools, 16 | # used for pkgs.path for nixos-option 17 | pkgs, 18 | }: let 19 | nixos-option = 20 | pkgs.nixos-option 21 | or (callPackage 22 | (pkgs.path + "/nixos/modules/installer/tools/nixos-option") {}); 23 | in 24 | runStrictCommand "project-manager" { 25 | preferLocalBuild = true; 26 | buildInputs = [bash-strict-mode]; 27 | nativeBuildInputs = [gettext]; 28 | meta = with lib; { 29 | mainProgram = "project-manager"; 30 | description = "A project environment configurator"; 31 | maintainers = [maintainers.sellout]; 32 | platforms = platforms.unix; 33 | license = licenses.mit; 34 | }; 35 | } '' 36 | install -v -D -m755 ${./project-manager} $out/bin/project-manager 37 | 38 | substituteInPlace $out/bin/project-manager \ 39 | --subst-var-by DEP_PATH "${ 40 | lib.makeBinPath [ 41 | bash-strict-mode 42 | coreutils 43 | findutils 44 | gettext 45 | gnused 46 | jq 47 | less 48 | ncurses 49 | nix 50 | nixos-option 51 | unixtools.hostname 52 | ] 53 | }" \ 54 | --subst-var-by OUT "$out" \ 55 | --subst-var-by VERSION "${release}" 56 | 57 | ## See NixOS/nixpkgs#247410. 58 | (set +u; patchShebangs --host $out/bin/project-manager) 59 | 60 | install -D -m755 ${./completion.bash} \ 61 | $out/share/bash-completion/completions/project-manager 62 | install -D -m755 ${./completion.zsh} \ 63 | $out/share/zsh/site-functions/_project-manager 64 | install -D -m755 ${./completion.fish} \ 65 | $out/share/fish/vendor_completions.d/project-manager.fish 66 | 67 | install -D -m755 ${../lib/bash/project-manager.bash} \ 68 | "$out/share/bash/project-manager.bash" 69 | 70 | substituteInPlace $out/share/bash/project-manager.bash \ 71 | --subst-var-by FLAKE_TEMPLATE '${../templates/default/flake.nix}' \ 72 | --subst-var-by CONFIG_TEMPLATE '${../templates/default/.config/project/default.nix}' 73 | 74 | # for path in {./po}/*.po; do 75 | # lang="''${path##*/}" 76 | # lang="''${lang%%.*}" 77 | # mkdir -p "$out/share/locale/$lang/LC_MESSAGES" 78 | # msgfmt -o "$out/share/locale/$lang/LC_MESSAGES/project-manager.mo" "$path" 79 | # done 80 | '' 81 | -------------------------------------------------------------------------------- /.config/project/github-pages.nix: -------------------------------------------------------------------------------- 1 | {lib, ...}: let 2 | defaultBranch = "main"; 3 | in { 4 | services.github = { 5 | settings.pages = { 6 | build_type = "workflow"; 7 | source.branch = defaultBranch; 8 | }; 9 | workflow."pages.yml".text = lib.pm.generators.toYAML {} { 10 | name = "Deploy generated docs to Pages"; 11 | on = { 12 | ## Runs on pushes targeting the default branch 13 | push.branches = [defaultBranch]; 14 | ## Allows you to run this workflow manually from the Actions tab 15 | workflow_dispatch = {}; 16 | }; 17 | 18 | ## Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages 19 | permissions = { 20 | contents = "read"; 21 | pages = "write"; 22 | id-token = "write"; 23 | }; 24 | 25 | ## Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued. 26 | ## However, do NOT cancel in-progress runs as we want to allow these production deployments to complete. 27 | concurrency = { 28 | group = "pages"; 29 | cancel-in-progress = false; 30 | }; 31 | 32 | jobs = { 33 | build = { 34 | runs-on = "ubuntu-24.04"; 35 | steps = [ 36 | { 37 | name = "Checkout"; 38 | uses = "actions/checkout@v6"; 39 | } 40 | { 41 | name = "Setup Pages"; 42 | uses = "actions/configure-pages@v4"; 43 | } 44 | { 45 | uses = "cachix/install-nix-action@v24"; 46 | "with".extra_nix_config = '' 47 | extra-trusted-public-keys = cache.garnix.io:CTFPyKSLcx5RMJKfLo5EEPUObbA78b0YQ2DTCJXqr9g= 48 | extra-substituters = https://cache.garnix.io 49 | ''; 50 | } 51 | { 52 | run = '' 53 | nix build .#docs-html 54 | cp -r result/share/doc/project-manager ./_site 55 | ''; 56 | } 57 | { 58 | name = "Upload artifact"; 59 | uses = "actions/upload-pages-artifact@v3"; 60 | } 61 | ]; 62 | }; 63 | deploy = { 64 | environment = { 65 | name = "github-pages"; 66 | url = "\${{ steps.deployment.outputs.page_url }}"; 67 | }; 68 | runs-on = "ubuntu-24.04"; 69 | needs = "build"; 70 | steps = [ 71 | { 72 | name = "Deploy to GitHub Pages"; 73 | id = "deployment"; 74 | uses = "actions/deploy-pages@v4"; 75 | } 76 | ]; 77 | }; 78 | }; 79 | }; 80 | }; 81 | } 82 | -------------------------------------------------------------------------------- /modules/misc/version.nix: -------------------------------------------------------------------------------- 1 | { 2 | config, 3 | lib, 4 | ... 5 | }: 6 | with lib; let 7 | releaseInfo = import ../../release.nix; 8 | in { 9 | options = { 10 | project.stateVersion = mkOption { 11 | type = types.ints.between 0 releaseInfo.version.major; 12 | description = '' 13 | It is occasionally necessary for Project Manager to change configuration 14 | defaults in a way that is incompatible with stateful data. This could, 15 | for example, include switching the default data format or location of a 16 | file. 17 | 18 | The *state version* indicates which default settings are in effect and 19 | will therefore help avoid breaking program configurations. Switching to 20 | a higher state version typically requires performing some manual steps, 21 | such as data conversion or moving files. 22 | 23 | The state version corresponds to the major version component of the 24 | Project Manager release where those defaults were specified. The value 25 | can be set to any Project Manager release up to and including this one. 26 | ''; 27 | }; 28 | 29 | project.version = { 30 | full = mkOption { 31 | internal = true; 32 | readOnly = true; 33 | type = types.str; 34 | default = let 35 | inherit (config.project.version) release revision; 36 | suffix = 37 | optionalString (revision != null) "+${substring 0 8 revision}"; 38 | in "${release}${suffix}"; 39 | example = "0.3.0+213a0629"; 40 | description = "The full Project Manager version."; 41 | }; 42 | 43 | release = mkOption { 44 | internal = true; 45 | readOnly = true; 46 | type = types.str; 47 | default = releaseInfo.release; 48 | example = "0.3.0"; 49 | description = "The Project Manager release."; 50 | }; 51 | 52 | isReleaseBranch = mkOption { 53 | internal = true; 54 | readOnly = true; 55 | type = types.bool; 56 | default = releaseInfo.isReleaseBranch; 57 | description = '' 58 | Whether the Project Manager version is from a versioned 59 | release branch. 60 | ''; 61 | }; 62 | 63 | revision = mkOption { 64 | internal = true; 65 | type = types.nullOr types.str; 66 | default = let 67 | gitRepo = "${toString ./../..}/.git"; 68 | in 69 | if pathIsGitRepo gitRepo 70 | then commitIdFromGitRepo gitRepo 71 | else null; 72 | description = '' 73 | The Git revision from which this Project Manager configuration was 74 | built. 75 | ''; 76 | }; 77 | }; 78 | }; 79 | } 80 | -------------------------------------------------------------------------------- /docs/manual/manpage-urls.json: -------------------------------------------------------------------------------- 1 | { 2 | "gnunet.conf(5)": "https://docs.gnunet.org/users/configuration.html", 3 | "mpd(1)": "https://mpd.readthedocs.io/en/latest/mpd.1.html", 4 | "mpd.conf(5)": "https://mpd.readthedocs.io/en/latest/mpd.conf.5.html", 5 | "nix.conf(5)": "https://nixos.org/manual/nix/stable/command-ref/conf-file.html", 6 | 7 | "journald.conf(5)": "https://www.freedesktop.org/software/systemd/man/journald.conf.html", 8 | "logind.conf(5)": "https://www.freedesktop.org/software/systemd/man/logind.conf.html", 9 | "networkd.conf(5)": "https://www.freedesktop.org/software/systemd/man/networkd.conf.html", 10 | "systemd.automount(5)": "https://www.freedesktop.org/software/systemd/man/systemd.automount.html", 11 | "systemd.exec(5)": "https://www.freedesktop.org/software/systemd/man/systemd.exec.html", 12 | "systemd.link(5)": "https://www.freedesktop.org/software/systemd/man/systemd.link.html", 13 | "systemd.mount(5)": "https://www.freedesktop.org/software/systemd/man/systemd.mount.html", 14 | "systemd.netdev(5)": "https://www.freedesktop.org/software/systemd/man/systemd.netdev.html", 15 | "systemd.network(5)": "https://www.freedesktop.org/software/systemd/man/systemd.network.html", 16 | "systemd.nspawn(5)": "https://www.freedesktop.org/software/systemd/man/systemd.nspawn.html", 17 | "systemd.path(5)": "https://www.freedesktop.org/software/systemd/man/systemd.path.html", 18 | "systemd.resource-control(5)": "https://www.freedesktop.org/software/systemd/man/systemd.resource-control.html", 19 | "systemd.scope(5)": "https://www.freedesktop.org/software/systemd/man/systemd.scope.html", 20 | "systemd.service(5)": "https://www.freedesktop.org/software/systemd/man/systemd.service.html", 21 | "systemd.slice(5)": "https://www.freedesktop.org/software/systemd/man/systemd.slice.html", 22 | "systemd.socket(5)": "https://www.freedesktop.org/software/systemd/man/systemd.socket.html", 23 | "systemd.timer(5)": "https://www.freedesktop.org/software/systemd/man/systemd.timer.html", 24 | "systemd.unit(5)": "https://www.freedesktop.org/software/systemd/man/systemd.unit.html", 25 | "systemd-system.conf(5)": "https://www.freedesktop.org/software/systemd/man/systemd-system.conf.html", 26 | "systemd-user.conf(5)": "https://www.freedesktop.org/software/systemd/man/systemd-user.conf.html", 27 | "timesyncd.conf(5)": "https://www.freedesktop.org/software/systemd/man/timesyncd.conf.html", 28 | "tmpfiles.d(5)": "https://www.freedesktop.org/software/systemd/man/tmpfiles.d.html", 29 | "systemd.time(7)": "https://www.freedesktop.org/software/systemd/man/systemd.time.html", 30 | "systemd-fstab-generator(8)": "https://www.freedesktop.org/software/systemd/man/systemd-fstab-generator.html", 31 | "systemd-networkd-wait-online.service(8)": "https://www.freedesktop.org/software/systemd/man/systemd-networkd-wait-online.service.html" 32 | } 33 | -------------------------------------------------------------------------------- /project-manager/completion.zsh: -------------------------------------------------------------------------------- 1 | #compdef project-manager 2 | 3 | local state ret=1 4 | 5 | _arguments \ 6 | '-I[search path]:PATH:_files -/' \ 7 | '--cores[cores]:NUM:()' \ 8 | '--debug[debug]' \ 9 | '--impure[impure]' \ 10 | '--keep-failed[keep failed]' \ 11 | '--keep-going[keep going]' \ 12 | '--version[version]' \ 13 | '(-h --help)'{--help,-h}'[help]' \ 14 | '(-v --verbose)'{--verbose,-v}'[verbose]' \ 15 | '(-n --dry-run)'{--dry-run,-n}'[dry run]' \ 16 | '(-f --file)'{--file,-f}'[configuration file]:FILE:_files' \ 17 | '(-j --max-jobs)'{--max-jobs,-j}'[max jobs]:NUM:()' \ 18 | '--option[option]:NAME VALUE:()' \ 19 | '--builders[builders]:SPEC:()' \ 20 | '(-L --print-build-logs)'{--print-build-logs,-L}'[print build logs]' \ 21 | '--show-trace[show trace]' \ 22 | '--override-input[override flake input]:NAME VALUE:()' \ 23 | '--update-input[update flake input]:NAME:()' \ 24 | '--experimental-features[set experimental Nix features]:VALUE:()' \ 25 | '--extra-experimental-features:[append to experimental Nix features]:VALUE:()' \ 26 | '1: :->cmds' \ 27 | '*:: :->args' && ret=0 28 | 29 | case "$state" in 30 | cmds) 31 | _values 'command' \ 32 | 'help[help]' \ 33 | 'edit[edit]' \ 34 | 'option[inspect option]' \ 35 | 'build[build]' \ 36 | 'init[init]' \ 37 | 'switch[switch]' \ 38 | 'generations[list generations]' \ 39 | 'remove-generations[remove generations]' \ 40 | 'expire-generations[expire generations]' \ 41 | 'packages[managed packages]' \ 42 | 'fmt[format]' \ 43 | 'news[read the news]' \ 44 | 'uninstall[uninstall]' && ret=0 45 | ;; 46 | args) 47 | case $line[1] in 48 | remove-generations) 49 | _values 'generations' \ 50 | $(project-manager generations | cut -d ' ' -f 5) && ret=0 51 | ;; 52 | build|switch) 53 | _arguments \ 54 | '--cores[cores]:NUM:()' \ 55 | '--debug[debug]' \ 56 | '--impure[impure]' \ 57 | '--keep-failed[keep failed]' \ 58 | '--keep-going[keep going]' \ 59 | '--max-jobs[max jobs]:NUM:()' \ 60 | '--no-out-link[no out link]' \ 61 | '--no-substitute[no substitute]' \ 62 | '--option[option]:NAME VALUE:()' \ 63 | '--show-trace[show trace]' \ 64 | '--substitute[substitute]' \ 65 | '--builders[builders]:SPEC:()' \ 66 | '--refresh[refresh]' \ 67 | '--override-input[override flake input]:NAME VALUE:()' \ 68 | '--update-input[update flake input]:NAME:()' \ 69 | '--experimental-features[set experimental Nix features]:VALUE:()' \ 70 | '--extra-experimental-features:[append to experimental Nix features]:VALUE:()' 71 | ;; 72 | init) 73 | _arguments \ 74 | '--switch[switch]' \ 75 | ':PATH:_files -/' 76 | ;; 77 | esac 78 | esac 79 | 80 | return ret 81 | -------------------------------------------------------------------------------- /modules/lib-bash/activation-init.bash: -------------------------------------------------------------------------------- 1 | function setupVars() { 2 | declare -r stateHome="$PROJECT_ROOT/.local/state" 3 | declare -r userNixStateDir="$stateHome/nix" 4 | declare -r pmGcrootsDir="$stateHome/project-manager/gcroots" 5 | 6 | mkdir -p "$userNixStateDir/profiles" 7 | declare -r profilesDir="$userNixStateDir/profiles" 8 | 9 | declare -gr genProfilePath="$profilesDir/project-manager" 10 | declare -gr newGenPath="@GENERATION_DIR@" 11 | declare -gr newGenGcPath="$pmGcrootsDir/current-project" 12 | 13 | declare greatestGenNum 14 | greatestGenNum=$( 15 | nix profile history --profile "$genProfilePath" \ 16 | | tail -2 \ 17 | | sed -E 's/.*m([[:digit:]]+).*/\1/' \ 18 | | head -1 19 | ) 20 | 21 | if [[ -n $greatestGenNum ]]; then 22 | declare -gr oldGenNum=$greatestGenNum 23 | declare -gr newGenNum=$((oldGenNum + 1)) 24 | else 25 | declare -gr newGenNum=1 26 | fi 27 | 28 | if [[ -e $genProfilePath ]]; then 29 | declare -g oldGenPath 30 | oldGenPath="$(readlink -e "$genProfilePath")" 31 | fi 32 | 33 | "${VERBOSE_RUN[@]}" _i "Checking oldGenNum and oldGenPath for consistency" 34 | if [[ -v oldGenNum && ! -v oldGenPath || 35 | ! -v oldGenNum && -v oldGenPath ]]; then 36 | _i $'The previous generation number and path are in conflict! These\nmust be either both empty or both set but are now set to\n\n \'%s\' and \'%s\'\n\nIf you don\'t mind losing previous profile generations then\nthe easiest solution is probably to run\n\n rm %s/project-manager*\n rm %s/current-project\n\nand trying project-manager switch again. Good luck!' \ 37 | "${oldGenNum-}" "${oldGenPath-}" \ 38 | "$profilesDir" "$pmGcrootsDir" 39 | exit 1 40 | fi 41 | } 42 | 43 | function checkProjectDirectory() { 44 | local expectedProject 45 | expectedProject=$(readlink --canonicalize-missing "$1") 46 | 47 | if ! [[ $PROJECT_ROOT -ef $expectedProject ]]; then 48 | _iError 'Error: PROJECT_ROOT is set to "%s" but we expect "%s"' "$PROJECT_ROOT" "$expectedProject" 49 | exit 1 50 | fi 51 | } 52 | 53 | pm_setVerboseAndDryRun 54 | 55 | _i "Starting Project Manager activation" 56 | 57 | # Verify that we can connect to the Nix store and/or daemon. This will 58 | # also create the necessary directories in profiles and gcroots. 59 | "${VERBOSE_RUN[@]}" _i "Validating Nix installation" 60 | nix-build --expr '{}' --no-out-link --quiet 61 | 62 | setupVars 63 | 64 | if [[ -v VERBOSE ]]; then 65 | _i 'Using Nix version: %s' "$(nix --version)" 66 | fi 67 | 68 | "${VERBOSE_RUN[@]}" _i "Activation variables:" 69 | if [[ -v oldGenNum ]]; then 70 | "${VERBOSE_ECHO}" " oldGenNum=$oldGenNum" 71 | "${VERBOSE_ECHO}" " oldGenPath=$oldGenPath" 72 | else 73 | "${VERBOSE_ECHO}" " oldGenNum undefined (first run?)" 74 | "${VERBOSE_ECHO}" " oldGenPath undefined (first run?)" 75 | fi 76 | "${VERBOSE_ECHO}" " newGenPath=$newGenPath" 77 | "${VERBOSE_ECHO}" " newGenNum=$newGenNum" 78 | "${VERBOSE_ECHO}" " genProfilePath=$genProfilePath" 79 | "${VERBOSE_ECHO}" " newGenGcPath=$newGenGcPath" 80 | -------------------------------------------------------------------------------- /modules/lib/types/tests/gvariant-merge.nix: -------------------------------------------------------------------------------- 1 | { 2 | config, 3 | lib, 4 | pkgs, 5 | ... 6 | }: let 7 | inherit 8 | (lib) 9 | concatStringsSep 10 | pm 11 | mapAttrsToList 12 | mkMerge 13 | mkOption 14 | types 15 | ; 16 | in { 17 | options.examples = mkOption {type = types.attrsOf pm.types.gvariant;}; 18 | 19 | config = { 20 | examples = with pm.gvariant; 21 | mkMerge [ 22 | {bool = true;} 23 | {bool = true;} 24 | 25 | {float = 3.14;} 26 | 27 | {int = -42;} 28 | {int = -42;} 29 | 30 | {uint32 = mkUint32 42;} 31 | {uint32 = mkUint32 42;} 32 | 33 | {int16 = mkInt16 (-42);} 34 | {int16 = mkInt16 (-42);} 35 | 36 | {uint16 = mkUint16 42;} 37 | {uint16 = mkUint16 42;} 38 | 39 | {int64 = mkInt64 (-42);} 40 | {int64 = mkInt64 (-42);} 41 | 42 | {uint64 = mkUint64 42;} 43 | {uint64 = mkUint64 42;} 44 | 45 | {array1 = ["one"];} 46 | {array1 = mkArray type.string ["two"];} 47 | {array2 = mkArray type.uint32 [1];} 48 | {array2 = mkArray type.uint32 [2];} 49 | 50 | {emptyArray1 = [];} 51 | {emptyArray2 = mkEmptyArray type.uint32;} 52 | 53 | {string = "foo";} 54 | {string = "foo";} 55 | { 56 | escapedString = '' 57 | '\ 58 | ''; 59 | } 60 | 61 | { 62 | tuple = mkTuple [ 63 | 1 64 | ["foo"] 65 | ]; 66 | } 67 | 68 | {maybe1 = mkNothing type.string;} 69 | {maybe2 = mkJust (mkUint32 4);} 70 | 71 | {variant1 = mkVariant "foo";} 72 | {variant2 = mkVariant 42;} 73 | 74 | { 75 | dictionaryEntry = mkDictionaryEntry [ 76 | 1 77 | ["foo"] 78 | ]; 79 | } 80 | ]; 81 | 82 | project.file."result.txt".text = let 83 | mkLine = n: v: "${n} = ${toString (pm.gvariant.mkValue v)}"; 84 | result = concatStringsSep "\n" (mapAttrsToList mkLine config.examples); 85 | in 86 | result + "\n"; 87 | 88 | nmt.script = '' 89 | assertFileContent \ 90 | project-files/result.txt \ 91 | ${pkgs.writeText "expected.txt" '' 92 | array1 = @as ['one','two'] 93 | array2 = @au [1,2] 94 | bool = true 95 | dictionaryEntry = @{ias} {1,@as ['foo']} 96 | emptyArray1 = @as [] 97 | emptyArray2 = @au [] 98 | escapedString = '\'\\\n' 99 | float = 3.140000 100 | int = -42 101 | int16 = @n -42 102 | int64 = @x -42 103 | maybe1 = @ms nothing 104 | maybe2 = just @u 4 105 | string = 'foo' 106 | tuple = @(ias) (1,@as ['foo']) 107 | uint16 = @q 42 108 | uint32 = @u 42 109 | uint64 = @t 42 110 | variant1 = @v <'foo'> 111 | variant2 = @v <42> 112 | ''} 113 | ''; 114 | }; 115 | } 116 | -------------------------------------------------------------------------------- /modules/programs/direnv.nix: -------------------------------------------------------------------------------- 1 | { 2 | config, 3 | lib, 4 | pkgs, 5 | ... 6 | }: 7 | with lib; let 8 | cfg = config.programs.direnv; 9 | 10 | projectDirectory = config.project.projectDirectory; 11 | 12 | fileContents = 13 | (import ../lib/file-type.nix { 14 | inherit projectDirectory lib pkgs; 15 | commit-by-default = config.project.commit-by-default; 16 | }) 17 | .fileContents; 18 | in { 19 | meta.maintainers = [maintainers.sellout]; 20 | 21 | options.programs.direnv = { 22 | enable = mkEnableOption "direnv, the environment switcher"; 23 | 24 | package = mkOption { 25 | type = types.package; 26 | default = pkgs.direnv; 27 | defaultText = lib.literalMD "pkgs.direnv"; 28 | description = '' 29 | Direnv package to install. 30 | ''; 31 | }; 32 | 33 | auto-allow = mkOption { 34 | type = types.bool; 35 | default = false; 36 | description = '' 37 | Whether running project-manager will implicitly run `direnv allow` for 38 | the user. 39 | ''; 40 | }; 41 | 42 | commit-envrc = mkOption { 43 | type = types.bool; 44 | default = true; 45 | description = '' 46 | Whether .envrc should be committed to the repo or only available 47 | locally. If this is false, then users will have to run 48 | `project-manager switch` before direnv will work. 49 | ''; 50 | }; 51 | 52 | envrc = mkOption { 53 | type = lib.types.lines; 54 | description = '' 55 | The contents of the .envrc file. 56 | ''; 57 | }; 58 | 59 | layoutDir = mkOption { 60 | type = lib.types.nullOr lib.types.str; 61 | default = "${config.xdg.cacheDir}/direnv"; 62 | description = '' 63 | Where to store direnv‘s cache. If null, this won’t be set. 64 | ''; 65 | }; 66 | }; 67 | 68 | config = mkIf cfg.enable { 69 | project.devPackages = [cfg.package]; 70 | 71 | programs.git.ignores = ["/${cfg.layoutDir}/"]; 72 | programs.mercurial.ignoresRooted = ["${cfg.layoutDir}/**"]; 73 | programs.vale.excludes = mkIf (config.project.file.".envrc".persistence != "store") [ 74 | "./${config.project.file.".envrc".target}" 75 | ]; 76 | 77 | project = { 78 | activation.direnv = mkIf cfg.auto-allow (pm.dag.entryAfter ["onFilesChange"] '' 79 | ${pkgs.direnv}/bin/direnv allow 80 | ''); 81 | 82 | file.".envrc" = { 83 | text = 84 | '' 85 | # This file was generated by Project Manager. 86 | '' 87 | + ( 88 | if cfg.layoutDir == null 89 | then "" 90 | else '' 91 | direnv_layout_dir="$PWD/${cfg.layoutDir}" 92 | '' 93 | ) 94 | + cfg.envrc; 95 | minimum-persistence = "worktree"; 96 | ## See direnv/direnv#1160. 97 | broken-symlink = true; 98 | commit-by-default = cfg.commit-envrc; 99 | }; 100 | }; 101 | }; 102 | } 103 | -------------------------------------------------------------------------------- /docs/manual/usage/configuration.md: -------------------------------------------------------------------------------- 1 | # Configuration Example {#sec-usage-configuration} 2 | 3 | A fresh install of Project Manager will generate a minimal `$PROJECT_ROOT/.config/project/default.nix` file containing something like 4 | 5 | ```nix 6 | { config, pkgs, ... }: 7 | 8 | { 9 | # This value determines the Project Manager release that your 10 | # configuration is compatible with. This helps avoid breakage 11 | # when a new Project Manager release introduces backwards 12 | # incompatible changes. 13 | # 14 | # You can update Project Manager without changing this value. See 15 | # the Project Manager release notes for a list of state version 16 | # changes in each release. 17 | home.stateVersion = 0; 18 | 19 | # Let Project Manager install and manage itself. 20 | programs.project-manager.enable = true; 21 | } 22 | ``` 23 | 24 | You can use this as a base for your further configurations. 25 | 26 | ::: {.note} 27 | If you aren’t very familiar with the Nix language and NixOS modules 28 | then it’s encouraged to start with small and simple changes. As you 29 | learn you can gradually grow the configuration with confidence. 30 | ::: 31 | 32 | As an example, let us expand the initial configuration file to also 33 | install the `htop` and `fortune` packages, install Emacs with a few extra 34 | packages available, and enable the user gpg-agent service. 35 | 36 | To satisfy the above setup we should elaborate the `project.nix` file as 37 | follows: 38 | 39 | ```nix 40 | { config, pkgs, ... }: 41 | 42 | { 43 | # Packages that should be installed to the project profile. 44 | project.devPackages = [ 45 | pkgs.htop 46 | pkgs.fortune 47 | ]; 48 | 49 | # This value determines the Project Manager release that your 50 | # configuration is compatible with. This helps avoid breakage 51 | # when a new Project Manager release introduces backwards 52 | # incompatible changes. 53 | # 54 | # You can update Project Manager without changing this value. See 55 | # the Project Manager release notes for a list of state version 56 | # changes in each release. 57 | project.stateVersion = 0; 58 | 59 | # Let Project Manager install and manage itself. 60 | programs.project-manager.enable = true; 61 | 62 | programs.emacs = { 63 | enable = true; 64 | extraPackages = epkgs: [ 65 | epkgs.nix-mode 66 | epkgs.magit 67 | ]; 68 | }; 69 | 70 | services.gpg-agent = { 71 | enable = true; 72 | defaultCacheTtl = 1800; 73 | enableSshSupport = true; 74 | }; 75 | } 76 | ``` 77 | 78 | - Nixpkgs packages can be installed to the development shell using 79 | [project.devPackages](#opt-project.devPackages). 80 | 81 | - The option names of a program module typically start with 82 | `programs.`. 83 | 84 | - Similarly, for a service module, the names start with 85 | `services.`. Note in some cases a package has both 86 | programs _and_ service options – Emacs is such an example. 87 | 88 | To activate this configuration you can run 89 | 90 | ```shell 91 | project-manager switch 92 | ``` 93 | 94 | or if you aren’t feeling so lucky, 95 | 96 | ```shell 97 | project-manager build 98 | ``` 99 | 100 | which will create a `result` link to a directory containing an 101 | activation script and the generated project directory files. 102 | -------------------------------------------------------------------------------- /templates/default/flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | description = "{{project.summary}}"; 3 | 4 | nixConfig = { 5 | registries = false; 6 | sandbox = "relaxed"; 7 | }; 8 | 9 | outputs = { 10 | flake-utils, 11 | nixpkgs, 12 | project-manager, 13 | self, 14 | }: 15 | flake-utils.lib.eachDefaultSystem (system: let 16 | pkgs = import nixpkgs {inherit system;}; 17 | in { 18 | ## This is the line that sets up Project Manager for your project. It 19 | ## expects to find the configuration in .config/project/default.nix, which 20 | ## is a Nix module. 21 | projectConfigurations = project-manager.lib.defaultConfiguration { 22 | inherit pkgs self; 23 | 24 | ## Specify any additional project configuration modules here, for 25 | ## example, ones from another flake. 26 | modules = [ 27 | # some-other-input.projectModules.someModule 28 | ]; 29 | 30 | ## Optionally use `extraSpecialArgs` to pass through arguments to 31 | ## project.nix. 32 | }; 33 | 34 | devShells = 35 | ## Project Manager provides some devShells, but not the default. The 36 | ## `project-manager` devShell contains the packages included in the 37 | ## project configuration (.config/project/default.nix). 38 | self.projectConfigurations.${system}.devShells 39 | // { 40 | ## An easy way to create a default devShell is to override 41 | ## `devShells.project-manager` and add the inputs from your other 42 | ## derivations. This should have all the build dependencies for your 43 | ## packages & checks, but also the packages from the project 44 | ## configuration. 45 | default = 46 | self.projectConfigurations.${system}.devShells.project-manager.overrideAttrs 47 | (old: { 48 | inputsFrom = 49 | old.inputsFrom 50 | or [] 51 | ++ builtins.attrValues self.checks.${system} or {} 52 | ++ builtins.attrValues self.packages.${system} or {}; 53 | }); 54 | }; 55 | 56 | ## If you’ve configured a formatter via your project configuration, this 57 | ## enables it for the flake – `nix fmt` will run this formatter across the 58 | ## project. 59 | formatter = self.projectConfigurations.${system}.formatter; 60 | 61 | ## Project Manager provides a number of its own checks – ones to ensure 62 | ## any generated files are up-to-date, ones that ensure the formatter has 63 | ## been run, etc. Project modules can also add their own checks – e.g., 64 | ## the Vale module adds a style check for English prose. 65 | checks = self.projectConfigurations.${system}.checks; 66 | }); 67 | 68 | inputs = { 69 | flake-utils.url = "github:numtide/flake-utils"; 70 | 71 | nixpkgs.url = "github:nixos/nixpkgs/release-24.11"; 72 | 73 | project-manager = { 74 | url = "github:sellout/project-manager"; 75 | ## It’s generally not helpful to set Project Manager’s nixpkgs to match 76 | ## your own. Project Manager distinguishes between the Nixpkgs it uses 77 | ## internally and the Nixpkgs used for your configuration. That allows 78 | ## Project Manager to support many different releases of Nixpkgs on a 79 | ## single develoment path. 80 | # inputs.nixpkgs.follows = "nixpkgs"; 81 | }; 82 | }; 83 | } 84 | -------------------------------------------------------------------------------- /modules/programs/mercurial.nix: -------------------------------------------------------------------------------- 1 | { 2 | config, 3 | lib, 4 | pkgs, 5 | ... 6 | }: 7 | with lib; let 8 | cfg = config.programs.mercurial; 9 | 10 | iniFormat = pkgs.formats.ini {}; 11 | in { 12 | meta.maintainers = [lib.maintainers.sellout]; 13 | 14 | options = { 15 | programs.mercurial = { 16 | enable = mkEnableOption "Mercurial"; 17 | 18 | package = mkOption { 19 | type = types.package; 20 | default = pkgs.mercurial; 21 | defaultText = lib.literalMD "pkgs.mercurial"; 22 | description = "Mercurial package to install."; 23 | }; 24 | 25 | aliases = mkOption { 26 | type = types.attrsOf types.anything; 27 | default = {}; 28 | description = "Mercurial aliases to define."; 29 | }; 30 | 31 | extraConfig = mkOption { 32 | type = types.either (types.attrsOf types.anything) types.lines; 33 | default = {}; 34 | description = "Additional configuration to add."; 35 | }; 36 | 37 | iniContent = mkOption { 38 | type = iniFormat.type; 39 | internal = true; 40 | }; 41 | 42 | ignores = mkOption { 43 | type = types.listOf types.str; 44 | default = []; 45 | example = ["*~" "*.swp"]; 46 | description = '' 47 | List of globs for files to be globally ignored. 48 | ''; 49 | }; 50 | 51 | ignoresRegexp = mkOption { 52 | type = types.listOf types.str; 53 | default = []; 54 | example = ["^.*~$" "^.*\\.swp$"]; 55 | description = '' 56 | List of regular expressions for files to be globally ignored. 57 | ''; 58 | }; 59 | 60 | ignoresRooted = mkOption { 61 | type = types.listOf types.str; 62 | default = []; 63 | example = ["^.*~$" "^.*\\.swp$"]; 64 | description = '' 65 | Similar to programs.mercurial.ignores, but these are rooted. 66 | ''; 67 | }; 68 | }; 69 | }; 70 | 71 | config = mkIf cfg.enable (mkMerge [ 72 | { 73 | project.devPackages = [cfg.package]; 74 | 75 | project.file.".hg/hgrc" = { 76 | persistence = "store"; 77 | source = iniFormat.generate "hgrc" cfg.iniContent; 78 | }; 79 | 80 | project.file.".hgignore" = { 81 | persistence = "store"; 82 | text = 83 | '' 84 | syntax: glob 85 | '' 86 | + concatStringsSep "\n" (mapAttrsToList (n: v: v.target) 87 | (filterAttrs (n: v: v.persistence != "repository") config.project.file) 88 | ++ cfg.ignores) 89 | + "\n" 90 | + '' 91 | syntax: regexp 92 | '' 93 | + concatStringsSep "\n" cfg.ignoresRegexp 94 | + "\n" 95 | + '' 96 | syntax: rootglob 97 | '' 98 | + concatStringsSep "\n" cfg.ignoresRooted 99 | + "\n"; 100 | }; 101 | } 102 | 103 | (mkIf (cfg.aliases != {}) { 104 | programs.mercurial.iniContent.alias = cfg.aliases; 105 | }) 106 | 107 | (mkIf (lib.isAttrs cfg.extraConfig) { 108 | programs.mercurial.iniContent = cfg.extraConfig; 109 | }) 110 | 111 | (mkIf (lib.isString cfg.extraConfig) { 112 | project.file.".hg/hgrc" = { 113 | persistence = "store"; 114 | text = cfg.extraConfig; 115 | }; 116 | }) 117 | ]); 118 | } 119 | -------------------------------------------------------------------------------- /testing/stubs.nix: -------------------------------------------------------------------------------- 1 | { 2 | config, 3 | lib, 4 | pkgs, 5 | ... 6 | }: let 7 | inherit (lib) mkOption types; 8 | 9 | stubType = types.submodule ( 10 | {name, ...}: { 11 | options = { 12 | name = mkOption { 13 | type = types.str; 14 | default = "dummy"; 15 | description = "The stub package name."; 16 | }; 17 | 18 | outPath = mkOption { 19 | type = types.nullOr types.str; 20 | default = "@${name}@"; 21 | defaultText = lib.literalExpression ''"@''${name}@"''; 22 | }; 23 | 24 | version = mkOption { 25 | type = types.nullOr types.str; 26 | default = null; 27 | defaultText = lib.literalExpression "pkgs.\${name}.version or null"; 28 | }; 29 | 30 | buildScript = mkOption { 31 | type = types.str; 32 | default = defaultBuildScript; 33 | }; 34 | 35 | extraAttrs = mkOption { 36 | type = types.attrsOf types.anything; 37 | default = {}; 38 | }; 39 | }; 40 | } 41 | ); 42 | 43 | defaultBuildScript = "mkdir $out"; 44 | 45 | dummyPackage = pkgs.runCommandLocal "dummy" {meta.mainProgram = "dummy";} defaultBuildScript; 46 | 47 | mkStubPackage = { 48 | name ? "dummy", 49 | outPath ? null, 50 | version ? null, 51 | buildScript ? defaultBuildScript, 52 | extraAttrs ? {}, 53 | }: let 54 | pkg = 55 | if name == "dummy" && buildScript == defaultBuildScript 56 | then dummyPackage 57 | else 58 | pkgs.runCommandLocal name { 59 | pname = name; 60 | meta.mainProgram = name; 61 | } 62 | buildScript; 63 | 64 | stubbedPkg = 65 | pkg 66 | // lib.optionalAttrs (outPath != null) { 67 | inherit outPath; 68 | 69 | # Prevent getOutput from descending into outputs 70 | outputSpecified = true; 71 | 72 | # Allow the original package to be used in derivation inputs 73 | __spliced = { 74 | buildHost = pkg; 75 | hostTarget = pkg; 76 | }; 77 | } 78 | // lib.optionalAttrs (version != null) {inherit version;} 79 | // extraAttrs; 80 | in 81 | stubbedPkg; 82 | in { 83 | options.test = { 84 | stubs = mkOption { 85 | type = types.attrsOf stubType; 86 | default = {}; 87 | description = "Package attributes that should be replaced by a stub package."; 88 | }; 89 | 90 | stubOverlays = mkOption { 91 | type = types.anything; 92 | default = []; 93 | internal = true; 94 | }; 95 | 96 | unstubs = mkOption { 97 | type = types.listOf types.anything; 98 | default = []; 99 | }; 100 | }; 101 | 102 | config = { 103 | lib.test.mkStubPackage = mkStubPackage; 104 | 105 | test.stubOverlays = 106 | [] 107 | ++ lib.optional (config.test.stubs != {}) ( 108 | self: super: 109 | lib.mapAttrs ( 110 | n: v: 111 | builtins.traceVerbose "${n} - stubbed" ( 112 | mkStubPackage ( 113 | v 114 | // lib.optionalAttrs (v.version == null) { 115 | version = super.${n}.version or null; 116 | } 117 | ) 118 | ) 119 | ) 120 | config.test.stubs 121 | ) 122 | ++ config.test.unstubs; 123 | }; 124 | } 125 | -------------------------------------------------------------------------------- /modules/misc/xdg.nix: -------------------------------------------------------------------------------- 1 | ## This module implements a pseudo-XDG file-system layout. It’s XDG base 2 | ## directories, but using `$PROJECT_ROOT` instead of `$HOME`. 3 | { 4 | config, 5 | lib, 6 | pkgs, 7 | ... 8 | }: let 9 | cfg = config.xdg; 10 | 11 | fileType = 12 | (import ../lib/file-type.nix { 13 | inherit lib pkgs; 14 | inherit (config.project) commit-by-default projectDirectory; 15 | }) 16 | .fileType; 17 | in { 18 | meta.maintainers = [lib.maintainers.sellout]; 19 | 20 | options.xdg = { 21 | cacheDir = lib.mkOption { 22 | type = lib.types.str; 23 | default = ".cache"; 24 | description = '' 25 | Path to directory holding application caches, relative to PROJECT_ROOT. 26 | ''; 27 | }; 28 | 29 | ## TODO: Maybe add a project option as to whether non-store persisted files 30 | ## should be placed in `xdg.cacheDir` when possible. Otherwise it 31 | ## would default to their standard locations. But we then need to be 32 | ## able to indicate in `fileType` whether a file is relocatable, and 33 | ## make sure the target & store paths are kept up-to-date. 34 | cacheFile = lib.mkOption { 35 | type = fileType "xdg.cacheFile" "{var}`xdg.cacheDir`" cfg.cacheDir; 36 | default = {}; 37 | description = '' 38 | Attribute set of files to link into the project's XDG 39 | cache directory. 40 | 41 | This should be used whenever files need to be included in the worktree, 42 | but we can control where they are located. It removes clutter from the 43 | worktree. 44 | ''; 45 | }; 46 | 47 | configDir = lib.mkOption { 48 | type = lib.types.str; 49 | default = ".config"; 50 | description = '' 51 | Path to directory holding application configurations, relative to 52 | PROJECT_ROOT. 53 | ''; 54 | }; 55 | 56 | configFile = lib.mkOption { 57 | type = fileType "xdg.configFile" "{var}`xdg.configDir`" cfg.configDir; 58 | default = {}; 59 | description = '' 60 | Attribute set of files to link into the project's XDG 61 | configuration directory. 62 | ''; 63 | }; 64 | 65 | dataDir = lib.mkOption { 66 | type = lib.types.str; 67 | default = ".local/share"; 68 | description = '' 69 | Path to directory holding application data, relative to PROJECT_ROOT. 70 | ''; 71 | }; 72 | 73 | dataFile = lib.mkOption { 74 | type = 75 | fileType "xdg.dataFile" "xdg.dataDir" cfg.dataDir; 76 | default = {}; 77 | description = '' 78 | Attribute set of files to link into the project's XDG 79 | data directory. 80 | ''; 81 | }; 82 | 83 | stateDir = lib.mkOption { 84 | type = lib.types.str; 85 | default = ".local/state"; 86 | description = '' 87 | Path to directory holding application states, relative to PROJECT_ROOT. 88 | ''; 89 | }; 90 | }; 91 | 92 | config = { 93 | project.file = lib.mkMerge [ 94 | (lib.mapAttrs' 95 | (name: file: lib.nameValuePair "${cfg.cacheDir}/${name}" file) 96 | cfg.cacheFile) 97 | (lib.mapAttrs' 98 | (name: file: lib.nameValuePair "${cfg.configDir}/${name}" file) 99 | cfg.configFile) 100 | (lib.mapAttrs' 101 | (name: file: lib.nameValuePair "${cfg.dataDir}/${name}" file) 102 | cfg.dataFile) 103 | ]; 104 | }; 105 | } 106 | -------------------------------------------------------------------------------- /docs/manual/quick-start/existing-project.md: -------------------------------------------------------------------------------- 1 | # Integrate With an Existing Project {#sec-existing-project} 2 | 3 | **WARNING**: `project-manager switch` (but no other subcommands) may overwrite files that already exist in your working tree. You should ensure that everything is committed or stashed before integrating Project Manager with an existing project. 4 | 5 | Start by creating a basic project configuration by running this from within your project directory: 6 | 7 | ```bash 8 | nix run github:sellout/project-manager 9 | ``` 10 | 11 | Note that this is similar to the command used by a new project. The differences are that 12 | 13 | 1. we’re not providing a directory for it to create, so it will use the current directory and 14 | 2. we’re not providing `--switch` to avoid it immediately creating the new configuration. 15 | 16 | If your project is already flake-based, this _won’t_ overwrite the existing flake, so you will need to modify your flake to provide Project Manager integration. 17 | 18 | At a minimum, you need to have 19 | 20 | ```nix 21 | { 22 | outputs = {nixpkgs, project-manager, self}: let 23 | system = "x86_64-linux"; # or another system 24 | pkgs = import nixpkgs {inherit system}; 25 | in { 26 | projectConfigurations.${system} = 27 | project-manager.lib.defaultConfiguration {inherit pkgs self}; 28 | } 29 | 30 | inputs = { 31 | # NB: This version doesn’t have to be current, Project Manager supports many 32 | # older Nixpkgs releases as well as unstable (as much as is possible). 33 | nixpkgs.url = "github:NixOS/nixpkgs/release-24.11"; 34 | project-manager.url = "github:sellout/project-manager"; 35 | } 36 | } 37 | ``` 38 | 39 | But project manager offers many attributes to integrate with other parts of your flake. For example., you may also include 40 | 41 | ```nix 42 | { 43 | outputs = {nixpkgs, project-manager, self}: let 44 | system = "x86_64-linux"; # or another system 45 | pkgs = import nixpkgs {inherit system}; 46 | in { 47 | # … 48 | 49 | devShells.${system} = let 50 | self.projectConfigurations.${system}.devShells 51 | // { 52 | # You can user `overrideAttrs` on this to extend this shell. 53 | default = 54 | self.projectConfigurations.${system}.devShells.project-manager; 55 | }; 56 | 57 | checks.${system} = 58 | # This provides a check that files generated by Project Manager are 59 | # up-to-date, as well as checks for other modules you may have 60 | # configured (a formatter, linters, etc.) 61 | self.projectConfigurations.${system}.checks 62 | // { 63 | # Any additional checks 64 | } 65 | 66 | # If your formatter is configured by Project Manager, which is especially 67 | # useful when using meta-formatters like treefmt-nix. 68 | # (https://github.com/numtide/treefmt-nix#readme) 69 | formatter.${system} = self.projectConfigurations.${system}.formatter; 70 | } 71 | ``` 72 | 73 | If you have all of your non-Project Manager related changes stashed, you can see how the default configuration affects your project by running 74 | 75 | ```nix 76 | nix run github:sellout/project-manager -- switch 77 | ``` 78 | 79 | You can now see what changes are reflected in your VCS. The most important thing is to check for modified files. For anything that has been modified you can either disable that module in .config/project/default.nix or adjust the [configuration](#ch-options) there to match what exists in your repository. 80 | 81 | Other files may have also been created. You can similarly disable these modules if you want to. 82 | -------------------------------------------------------------------------------- /.github/settings.yml: -------------------------------------------------------------------------------- 1 | # This file was generated by Project Manager. 2 | {"branches":[{"name":"main","protection":{"allow_force_pushes":false,"enforce_admins":true,"required_linear_history":false,"required_pull_request_reviews":null,"required_status_checks":{"contexts":["All Garnix checks","build checks.x86_64-linux.formatter","build checks.x86_64-linux.project-manager-files","build checks.x86_64-linux.shellcheck","build checks.x86_64-linux.vale","build devShells.x86_64-linux.default","build devShells.x86_64-linux.project-manager","build packages.x86_64-linux.default","build packages.x86_64-linux.docs-html","build packages.x86_64-linux.docs-json","build packages.x86_64-linux.docs-manpages","build packages.x86_64-linux.project-manager","build checks.x86_64-linux.formatter-22_11","build checks.x86_64-linux.project-manager-files-22_11","build checks.x86_64-linux.shellcheck-22_11","build checks.x86_64-linux.vale-22_11","build checks.x86_64-linux.formatter-23_05","build checks.x86_64-linux.project-manager-files-23_05","build checks.x86_64-linux.shellcheck-23_05","build checks.x86_64-linux.vale-23_05","build checks.x86_64-linux.formatter-23_11","build checks.x86_64-linux.project-manager-files-23_11","build checks.x86_64-linux.shellcheck-23_11","build checks.x86_64-linux.vale-23_11","build checks.x86_64-linux.formatter-24_05","build checks.x86_64-linux.project-manager-files-24_05","build checks.x86_64-linux.shellcheck-24_05","build checks.x86_64-linux.vale-24_05","build checks.x86_64-linux.formatter-24_11","build checks.x86_64-linux.project-manager-files-24_11","build checks.x86_64-linux.shellcheck-24_11","build checks.x86_64-linux.vale-24_11","build checks.x86_64-linux.formatter-25_05","build checks.x86_64-linux.project-manager-files-25_05","build checks.x86_64-linux.shellcheck-25_05","build checks.x86_64-linux.vale-25_05","build checks.x86_64-linux.formatter-25_11","build checks.x86_64-linux.project-manager-files-25_11","build checks.x86_64-linux.shellcheck-25_11","build checks.x86_64-linux.vale-25_11"],"strict":false},"restrictions":null}}],"labels":[{"color":"#666666","description":"Created automatically by some service or process","name":"automated"},{"color":"#d73a4a","description":"Something isn’t working","name":"bug"},{"color":"#333333","description":"Updates or other changes to dependencies","name":"dependencies"},{"color":"#0075ca","description":"Improvements or additions to documentation","name":"documentation"},{"color":"#a2eeef","description":"New feature or request","name":"enhancement"},{"color":"#7057ff","description":"Good for newcomers","name":"good first issue"},{"color":"#000000","description":"Issues you want contributors to help with.","name":"hacktoberfest"},{"color":"#ff7518","description":"Indicates acceptance for Hacktoberfest criteria, even if not merged yet.","name":"hacktoberfest-accepted"},{"color":"#008672","description":"Extra attention is needed","name":"help wanted"},{"color":"#333333","description":"Unaccepted contributions that haven’t been closed for some reason.","name":"invalid"},{"color":"#d876e3","description":"Further information is requested","name":"question"},{"color":"#ffc0cb","description":"Topic created in bad faith. Services like Hacktoberfest use this to identify bad actors.","name":"spam"},{"color":"#d4af37","description":"Work prioritized by a sponsor","name":"sponsored"}],"pages":{"build_type":"workflow","source":{"branch":"main"}},"repository":{"allow_merge_commit":true,"allow_rebase_merge":false,"allow_squash_merge":false,"default_branch":"main","delete_branch_on_merge":true,"description":"Home Manager, but for repos.","enable_automated_security_fixes":true,"enable_vulnerability_alerts":true,"has_downloads":false,"has_issues":true,"has_projects":true,"has_wiki":true,"homepage":"https://sellout.github.io/project-manager","merge_commit_message":"PR_BODY","merge_commit_title":"PR_TITLE","name":"project-manager","private":false,"topics":"hacktoberfest, development, nix-flakes"}} 3 | -------------------------------------------------------------------------------- /modules/services/flakehub/default.nix: -------------------------------------------------------------------------------- 1 | { 2 | config, 3 | lib, 4 | pkgs, 5 | ... 6 | }: let 7 | cfg = config.services.flakehub; 8 | in { 9 | meta.maintainers = [lib.maintainers.sellout]; 10 | 11 | options.services.flakehub = { 12 | enable = lib.mkEnableOption "FlakeHub"; 13 | 14 | package = lib.mkPackageOption pkgs "FlakeHub" { 15 | default = ["fh"]; 16 | }; 17 | 18 | mode = lib.mkOption { 19 | type = 20 | lib.types.either (lib.types.enum ["tagged"]) 21 | (lib.types.attrsOf lib.types.str); 22 | default = "tagged"; 23 | example.rolling = "main"; 24 | description = '' 25 | This is either the string “tagged” or an attrset with the key “rolling” 26 | whose value names the branch to watch for rolling changes. 27 | ''; 28 | }; 29 | 30 | name = lib.mkOption { 31 | description = '' 32 | The name to publish as on FlakeHub. This defaults to “owner/repo”. 33 | ''; 34 | type = lib.types.str; 35 | # default = {}; # TODO: Infer from the upstream / origin (default?) remote 36 | }; 37 | 38 | visibility = lib.mkOption { 39 | description = '' 40 | “unlisted” means that your flake can be accessed by Nix but only appears 41 | on the website if you directly navigate to it. “public” means that your 42 | flake can be found by searching. 43 | ''; 44 | type = lib.types.enum ["unlisted" "public"]; 45 | default = "unlisted"; 46 | }; 47 | }; 48 | 49 | config = lib.mkIf cfg.enable { 50 | ## Written as `lines` instead of an attr set to make it easier to compare 51 | ## against https://flakehub.com/new, which this mimics. 52 | services.github.workflow."flakehub-publish.yml" = { 53 | text = 54 | if cfg.mode ? rolling 55 | then '' 56 | name: "Publish every Git push to ${cfg.mode.rolling} to FlakeHub" 57 | on: 58 | push: 59 | branches: 60 | - "${cfg.mode.rolling}" 61 | jobs: 62 | flakehub-publish: 63 | runs-on: "ubuntu-24.04" 64 | permissions: 65 | id-token: "write" 66 | contents: "read" 67 | steps: 68 | - uses: "actions/checkout@v6" 69 | - uses: "DeterminateSystems/nix-installer-action@main" 70 | - uses: "DeterminateSystems/flakehub-push@main" 71 | with: 72 | name: "${cfg.name}" 73 | rolling: true 74 | visibility: "${cfg.visibility}" 75 | '' 76 | else '' 77 | name: "Publish tags to FlakeHub" 78 | on: 79 | push: 80 | tags: 81 | - "v?[0-9]+.[0-9]+.[0-9]+*" 82 | workflow_dispatch: 83 | inputs: 84 | tag: 85 | description: "The existing tag to publish to FlakeHub" 86 | type: "string" 87 | required: true 88 | jobs: 89 | flakehub-publish: 90 | runs-on: "ubuntu-24.04" 91 | permissions: 92 | id-token: "write" 93 | contents: "read" 94 | steps: 95 | - uses: "actions/checkout@v6" 96 | with: 97 | ref: "''${{ (inputs.tag != null) && format('refs/tags/{0}', inputs.tag) || ''' }}" 98 | - uses: "DeterminateSystems/nix-installer-action@main" 99 | - uses: "DeterminateSystems/flakehub-push@main" 100 | with: 101 | visibility: "${cfg.visibility}" 102 | name: "${cfg.name}" 103 | tag: "''${{ inputs.tag }}" 104 | ''; 105 | }; 106 | }; 107 | } 108 | -------------------------------------------------------------------------------- /nix/schemas.nix: -------------------------------------------------------------------------------- 1 | {flake-schemas}: let 2 | ## Stolen from DeterminateSystems/flake-schemas, which should export it in its 3 | ## lib. 4 | checkDerivation = drv: drv.type or null == "derivation" && drv ? drvPath; 5 | in { 6 | projectConfigurations = { 7 | version = 1; 8 | doc = '' 9 | The `projectConfigurations` flake output defines project configurations. 10 | ''; 11 | inventory = output: 12 | flake-schemas.lib.mkChildren (builtins.mapAttrs (system: project: let 13 | forSystems = [system]; 14 | in { 15 | inherit forSystems; 16 | what = "Project Manager configuration for this flake’s project"; 17 | children = { 18 | packages = flake-schemas.lib.mkChildren (builtins.mapAttrs (packageName: package: { 19 | inherit forSystems; 20 | shortDescription = package.meta.description or ""; 21 | derivation = package; 22 | evalChecks.isDerivation = checkDerivation package; 23 | what = "package"; 24 | isFlakeCheck = false; 25 | }) 26 | project.packages); 27 | checks = flake-schemas.lib.mkChildren (builtins.mapAttrs (checkName: check: { 28 | inherit forSystems; 29 | shortDescription = check.meta.description or ""; 30 | derivation = check; 31 | evalChecks.isDerivation = checkDerivation check; 32 | what = "CI test"; 33 | isFlakeCheck = true; 34 | }) 35 | project.checks); 36 | devShells = flake-schemas.lib.mkChildren (builtins.mapAttrs (shellName: shell: { 37 | inherit forSystems; 38 | shortDescription = shell.meta.description or ""; 39 | derivation = shell; 40 | evalChecks.isDerivation = checkDerivation shell; 41 | what = "development environment"; 42 | isFlakeCheck = false; 43 | }) 44 | project.devShells); 45 | formatter = { 46 | inherit forSystems; 47 | shortDescription = project.formatter.meta.description or ""; 48 | derivation = project.formatter; 49 | evalChecks.isDerivation = checkDerivation project.formatter; 50 | what = "Nix code formatter"; 51 | isFlakeCheck = false; 52 | }; 53 | filterRepositoryPersistedExcept = { 54 | inherit forSystems; 55 | shortDescription = project.options.project.filterRepositoryPersistedExcept.description; 56 | what = "source filter"; 57 | isFlakeCheck = false; 58 | }; 59 | filterRepositoryPersisted = { 60 | inherit forSystems; 61 | shortDescription = project.options.project.filterRepositoryPersisted.description; 62 | what = "source filter"; 63 | isFlakeCheck = false; 64 | }; 65 | cleanRepositoryPersistedExcept = { 66 | inherit forSystems; 67 | shortDescription = project.options.project.cleanRepositoryPersistedExcept.description; 68 | what = "source filter"; 69 | isFlakeCheck = false; 70 | }; 71 | cleanRepositoryPersisted = { 72 | inherit forSystems; 73 | shortDescription = project.options.project.cleanRepositoryPersisted.description; 74 | what = "source filter"; 75 | isFlakeCheck = false; 76 | }; 77 | }; 78 | }) 79 | output); 80 | }; 81 | 82 | projectModules = { 83 | version = 1; 84 | doc = '' 85 | Defines “project modules” analogous to `nixosModules` or 86 | `homeModules`, but scoped to a single project (often some VCS repo). 87 | ''; 88 | inventory = output: 89 | flake-schemas.lib.mkChildren (builtins.mapAttrs (moduleName: module: { 90 | what = "Project Manager module"; 91 | }) 92 | output); 93 | }; 94 | } 95 | -------------------------------------------------------------------------------- /testing/darwinScrubList.nix: -------------------------------------------------------------------------------- 1 | { 2 | lib, 3 | scrubDerivation, 4 | }: let 5 | # List of packages that need to be scrubbed on Darwin 6 | # Packages are scrubbed on Linux and expected in test output 7 | packagesToScrub = [ 8 | # keep-sorted start case=no numeric=yes 9 | "aerc" 10 | "aerospace" 11 | "aichat" 12 | "alacritty" 13 | "alot" 14 | "antidote" 15 | "aria2" 16 | "atuin" 17 | "autojump" 18 | "bacon" 19 | "bash" 20 | "bash-completion" 21 | "bash-preexec" 22 | "bashInteractive" 23 | "bat" 24 | "borgmatic" 25 | "bottom" 26 | "broot" 27 | "browserpass" 28 | "btop" 29 | "carapace" 30 | "cava" 31 | "clock-rs" 32 | "cmus" 33 | "codex" 34 | "comodoro" 35 | "darcs" 36 | "delta" 37 | "dircolors" 38 | "direnv" 39 | "earthly" 40 | "element-desktop" 41 | "emacs" 42 | "espanso" 43 | "eza" 44 | "fastfetch" 45 | "feh" 46 | "fzf" 47 | "gallery-dl" 48 | "gh" 49 | "gh-dash" 50 | "ghostty" 51 | "git" 52 | "git-cliff" 53 | "git-credential-oauth" 54 | "git-lfs" 55 | "git-worktree-switcher" 56 | "gitMinimal" 57 | "gnupg" 58 | "go" 59 | "gradle" 60 | "granted" 61 | "helix" 62 | "hello" 63 | "himalaya" 64 | "hjson-go" 65 | "htop" 66 | "hyfetch" 67 | "i3status" 68 | "inori" 69 | "irssi" 70 | "isync" 71 | "jankyborders" 72 | "joplin-desktop" 73 | "jqp" 74 | "jujutsu" 75 | "k9s" 76 | "kakoune" 77 | "khal" 78 | "khard" 79 | "kitty" 80 | "kubecolor" 81 | "kubeswitch" 82 | "lapce" 83 | "lazydocker" 84 | "lazygit" 85 | "ledger" 86 | "less" 87 | "lesspipe" 88 | "lf" 89 | "lieer" 90 | "lsd" 91 | "mbsync" 92 | "meli" 93 | "mergiraf" 94 | "micro" 95 | "mise" 96 | "mpv" 97 | "msmtp" 98 | "mu" 99 | "mujmap" 100 | "mullvad-vpn" 101 | "ncmpcpp" 102 | "ne" 103 | "neomutt" 104 | "neovide" 105 | "neovim" 106 | "newsboat" 107 | "nh" 108 | "nheko" 109 | "nix" 110 | "nix-direnv" 111 | "nix-index" 112 | "nix-init" 113 | "nix-your-shell" 114 | "notmuch" 115 | "npth" 116 | "nushell" 117 | "nyxt" 118 | "oh-my-posh" 119 | "ollama" 120 | "onlyoffice-desktopeditors" 121 | "opencode" 122 | "openstackclient" 123 | "papis" 124 | "patdiff" 125 | "pay-respects" 126 | "pet" 127 | "pistol" 128 | "pls" 129 | "poetry" 130 | "powerline-go" 131 | "pubs" 132 | "pyenv" 133 | "qcal" 134 | "qutebrowser" 135 | "ranger" 136 | "rio" 137 | "ripgrep" 138 | "ruff" 139 | "sage" 140 | "sapling" 141 | "sbt" 142 | "scmpuff" 143 | "senpai" 144 | "sftpman" 145 | "sioyek" 146 | "skhd" 147 | "sm64ex" 148 | "smug" 149 | "spotify-player" 150 | "starship" 151 | "superfile" 152 | "taskwarrior" 153 | "tealdeer" 154 | "texlive" 155 | "thefuck" 156 | "thunderbird" 157 | "tmate" 158 | "tmux" 159 | "topgrade" 160 | "translate-shell" 161 | "tray-tui" 162 | "vifm" 163 | "vim-vint" 164 | "vimPlugins" 165 | "visidata" 166 | "vscode" 167 | "wallust" 168 | "watson" 169 | "wezterm" 170 | "yazi" 171 | "yq-go" 172 | "yubikey-agent" 173 | "zed-editor" 174 | "zellij" 175 | "zk" 176 | "zoxide" 177 | "zplug" 178 | "zsh" 179 | # keep-sorted end 180 | ]; 181 | 182 | # Create an overlay that scrubs packages in the scrublist 183 | packageScrubOverlay = self: super: 184 | lib.mapAttrs ( 185 | name: value: 186 | if lib.elem name packagesToScrub 187 | then 188 | # Apply scrubbing to this specific package 189 | scrubDerivation name value 190 | else value 191 | ) 192 | super; 193 | in 194 | self: super: 195 | packageScrubOverlay self super 196 | // { 197 | buildPackages = super.buildPackages.extend packageScrubOverlay; 198 | } 199 | -------------------------------------------------------------------------------- /modules/misc/news.nix: -------------------------------------------------------------------------------- 1 | { 2 | config, 3 | lib, 4 | pkgs, 5 | ... 6 | }: 7 | with lib; let 8 | cfg = config.news; 9 | 10 | hostPlatform = pkgs.stdenv.hostPlatform; 11 | 12 | entryModule = types.submodule ({config, ...}: { 13 | options = { 14 | id = mkOption { 15 | internal = true; 16 | type = types.str; 17 | description = '' 18 | A unique entry identifier. By default it is a base16 19 | formatted hash of the entry message. 20 | ''; 21 | }; 22 | 23 | time = mkOption { 24 | internal = true; 25 | type = types.str; 26 | example = "2017-07-10T21:55:04+00:00"; 27 | description = '' 28 | News entry time stamp in ISO-8601 format. Must be in UTC 29 | (ending in '+00:00'). 30 | ''; 31 | }; 32 | 33 | condition = mkOption { 34 | internal = true; 35 | default = true; 36 | description = "Whether the news entry should be active."; 37 | }; 38 | 39 | message = mkOption { 40 | internal = true; 41 | type = types.str; 42 | description = "The news entry content."; 43 | }; 44 | }; 45 | 46 | config = { 47 | id = mkDefault (builtins.hashString "sha256" config.message); 48 | }; 49 | }); 50 | in { 51 | meta.maintainers = [ 52 | maintainers.sellout 53 | maintainers.rycee 54 | ]; 55 | 56 | options = { 57 | news = { 58 | display = mkOption { 59 | type = types.enum ["silent" "notify" "show"]; 60 | default = "notify"; 61 | description = '' 62 | How unread and relevant news should be presented when 63 | running {command}`project-manager build` and 64 | {command}`project-manager switch`. 65 | 66 | The options are 67 | 68 | `silent` 69 | : Do not print anything during build or switch. The 70 | {command}`project-manager news` command still 71 | works for viewing the entries. 72 | 73 | `notify` 74 | : The number of unread and relevant news entries will be 75 | printed to standard output. The {command}`project-manager 76 | news` command can later be used to view the entries. 77 | 78 | `show` 79 | : A pager showing unread news entries is opened. 80 | ''; 81 | }; 82 | 83 | entries = mkOption { 84 | internal = true; 85 | type = types.listOf entryModule; 86 | default = []; 87 | description = "News entries."; 88 | }; 89 | 90 | json = { 91 | output = mkOption { 92 | internal = true; 93 | type = types.package; 94 | description = "The generated JSON file package."; 95 | }; 96 | }; 97 | }; 98 | }; 99 | 100 | config = { 101 | news.json.output = pkgs.writeText "pm-news.json" (builtins.toJSON { 102 | inherit (cfg) display entries; 103 | }); 104 | 105 | # Add news entries in chronological order (i.e., latest time 106 | # should be at the bottom of the list). The time should be 107 | # formatted as given in the output of 108 | # 109 | # date --iso-8601=second --universal 110 | # 111 | # On darwin (or BSD like systems) use 112 | # 113 | # date -u +'%Y-%m-%dT%H:%M:%S+00:00' 114 | news.entries = [ 115 | { 116 | time = "2023-07-25T07:16:09+00:00"; 117 | condition = hostPlatform.isDarwin; 118 | message = '' 119 | A new module is available: 'services.git-sync'. 120 | ''; 121 | } 122 | 123 | { 124 | time = "2023-08-15T15:45:45+00:00"; 125 | message = '' 126 | A new module is available: 'programs.xplr'. 127 | ''; 128 | } 129 | 130 | { 131 | time = "2023-08-16T15:43:30+00:00"; 132 | message = '' 133 | A new module is available: 'programs.pqiv'. 134 | ''; 135 | } 136 | 137 | { 138 | time = "2023-08-22T16:06:52+00:00"; 139 | message = '' 140 | A new module is available: 'programs.qcal'. 141 | ''; 142 | } 143 | ]; 144 | }; 145 | } 146 | -------------------------------------------------------------------------------- /modules/services/garnix.nix: -------------------------------------------------------------------------------- 1 | { 2 | config, 3 | flaky, 4 | lib, 5 | pkgs, 6 | self, 7 | supportedSystems, 8 | ... 9 | }: 10 | with lib; let 11 | cfg = config.services.garnix; 12 | 13 | isUniversalAttr = value: lib.elem "*.*" value && lib.elem "*.*.*" value; 14 | 15 | buildOptions.options = { 16 | exclude = lib.mkOption { 17 | type = lib.pm.types.globList isUniversalAttr; 18 | default = []; 19 | description = '' 20 | A list of attributes with wildcards that garnix should skip. 21 | ''; 22 | }; 23 | include = lib.mkOption { 24 | type = lib.pm.types.globList isUniversalAttr; 25 | default = []; 26 | description = '' 27 | A list of attributes with wildcards that garnix should not skip. 28 | ''; 29 | }; 30 | }; 31 | serverOptions.options = { 32 | configuration = lib.mkOption { 33 | type = lib.types.str; 34 | description = '' 35 | The name of the `nixosConfiguration` to deploy. 36 | ''; 37 | }; 38 | branch = lib.mkOption { 39 | type = lib.types.nullOr lib.types.str; 40 | default = null; 41 | description = '' 42 | This is set to either the branch name for an `on-branch` deployment, or 43 | `null` for `on-pull-request` deployment. 44 | ''; 45 | }; 46 | }; 47 | in { 48 | meta.maintainers = [maintainers.sellout]; 49 | 50 | options.services.garnix = { 51 | enable = mkEnableOption "Garnix CI configuration"; 52 | 53 | builds = mkOption { 54 | type = lib.types.attrsOf (lib.types.submodule buildOptions); 55 | default = {}; 56 | description = '' 57 | Configuration written to {file}`$PROJECT_ROOT/garnix.yaml`. 58 | See for documentation. 59 | 60 | The structure here is different from the YAML. Instead of a “branch” 61 | field, this makes the branch names the keys of the attrSet, with `"*"` 62 | meaning “all branches”. 63 | ''; 64 | example = lib.literalMD '' 65 | { 66 | "*" = { 67 | exclude = ["homeConfigurations.*"]; 68 | include = [ 69 | "*.x86_64-linux.*" 70 | "packages.aarch64-darwin.*" 71 | "defaultPackage.x86_64-linux" 72 | "devShell.x86_64-linux" 73 | ]; 74 | }; 75 | main.include = ["packages.*.release"]; 76 | } 77 | ''; 78 | }; 79 | 80 | servers = mkOption { 81 | type = lib.types.listOf (lib.types.submodule serverOptions); 82 | default = []; 83 | description = '' 84 | Configuration written to {file}`$PROJECT_ROOT/garnix.yaml`. 85 | See and for documentation. 86 | ''; 87 | example = lib.literalMD '' 88 | [ 89 | { 90 | configuration = "example"; 91 | branch = "main"; 92 | } 93 | {configuration = "test";} 94 | ]; 95 | ''; 96 | }; 97 | }; 98 | 99 | config = mkIf (cfg.enable) { 100 | project.file."garnix.yaml".text = lib.pm.generators.toYAML {} { 101 | builds = lib.mapAttrsToList (branch: value: 102 | if branch == "*" 103 | then value 104 | else value // {inherit branch;}) 105 | cfg.builds; 106 | servers = 107 | map ({ 108 | configuration, 109 | branch, 110 | }: { 111 | inherit configuration; 112 | deployment = 113 | if branch == null 114 | then {type = "on-pull-request";} 115 | else { 116 | inherit branch; 117 | type = "on-branch"; 118 | }; 119 | }) 120 | cfg.servers; 121 | }; 122 | 123 | ## Can’t build un-sandboxed derivations on Garnix (see garnix-io/issues#33) 124 | services.garnix.builds."*" = { 125 | exclude = flaky.lib.forGarnixSystems supportedSystems (sys: 126 | map 127 | (name: "checks.${sys}.${name}") 128 | (builtins.attrNames 129 | self.projectConfigurations.${sys}.unsandboxedChecks) 130 | ++ ["devShells.${sys}.lax-checks"]); 131 | }; 132 | }; 133 | } 134 | -------------------------------------------------------------------------------- /project-manager/completion.fish: -------------------------------------------------------------------------------- 1 | #!/bin/env fish 2 | ################################################## 3 | 4 | # « project-manager » command-line fish completion 5 | # 6 | # © 2021 "Ariel AxionL" 7 | # 8 | # MIT License 9 | # 10 | 11 | ################################################## 12 | 13 | ### Functions 14 | function __project_manager_generations --description "Get all generations" 15 | for i in (project-manager generations) 16 | set -l split (string split " " $i) 17 | set -l gen_id $split[5] 18 | set -l gen_datetime $split[1..2] 19 | set -l gen_hash (string match -r '\w{32}' $i) 20 | echo $gen_id\t$gen_datetime $gen_hash 21 | end 22 | end 23 | 24 | 25 | ### SubCommands 26 | complete -c project-manager -n "__fish_use_subcommand" -f -a "help" -d "Print project-manager help" 27 | complete -c project-manager -n "__fish_use_subcommand" -f -a "edit" -d "Open the project configuration in $EDITOR" 28 | complete -c project-manager -n "__fish_use_subcommand" -f -a "build" -d "Build configuration into result directory" 29 | complete -c project-manager -n "__fish_use_subcommand" -f -a "switch" -d "Build and activate configuration" 30 | complete -c project-manager -n "__fish_use_subcommand" -f -a "generations" -d "List all project environment generations" 31 | complete -c project-manager -n "__fish_use_subcommand" -f -a "packages" -d "List all packages installed in project-manager-path" 32 | complete -c project-manager -n "__fish_use_subcommand" -f -a "fmt" -d "Run the configured formatter. This is usually much faster than running ‘nix fmt’ because it avoids evaluating the flake." 33 | complete -c project-manager -n "__fish_use_subcommand" -f -a "news" -d "Show news entries in a pager" 34 | complete -c project-manager -n "__fish_use_subcommand" -f -a "uninstall" -d "Remove Project Manager" 35 | 36 | complete -c project-manager -n "__fish_use_subcommand" -x -a "remove-generations" -d "Remove indicated generations" 37 | complete -c project-manager -n "__fish_seen_subcommand_from remove-generations" -f -ka '(__project_manager_generations)' 38 | 39 | complete -c project-manager -n "__fish_use_subcommand" -x -a "expire-generations" -d "Remove generations older than TIMESTAMP" 40 | 41 | ### Options 42 | complete -c project-manager -F -s f -l "file" -d "The project configuration file" 43 | complete -c project-manager -F -s I -d "Add a path to the Nix expression search path" 44 | complete -c project-manager -F -l "flake" -d "Use Project Manager configuration at specified flake-uri" 45 | complete -c project-manager -f -s v -l "verbose" -d "Verbose output" 46 | complete -c project-manager -f -s n -l "dry-run" -d "Do a dry run, only prints what actions would be taken" 47 | complete -c project-manager -f -s h -l "help" -d "Print this help" 48 | complete -c project-manager -f -s h -l "version" -d "Print the Project Manager version" 49 | 50 | complete -c project-manager -x -l "arg" -d "Override inputs passed to project-manager.nix" 51 | complete -c project-manager -x -l "argstr" -d "Like --arg but the value is a string" 52 | complete -c project-manager -x -l "cores" -d "Threads per job (e.g. -j argument to make)" 53 | complete -c project-manager -x -l "debug" 54 | complete -c project-manager -x -l "impure" 55 | complete -c project-manager -f -l "keep-failed" -d "Keep temporary directory used by failed builds" 56 | complete -c project-manager -f -l "keep-going" -d "Keep going in case of failed builds" 57 | complete -c project-manager -x -s j -l "max-jobs" -d "Max number of build jobs in parallel" 58 | complete -c project-manager -x -l "option" -d "Set Nix configuration option" 59 | complete -c project-manager -x -l "builders" -d "Remote builders" 60 | complete -c project-manager -f -s L -l "print-build-logs" -d "Print full build logs on standard error" 61 | complete -c project-manager -f -l "show-trace" -d "Print stack trace of evaluation errors" 62 | complete -c project-manager -f -l "substitute" 63 | complete -c project-manager -f -l "no-substitute" 64 | complete -c project-manager -f -l "no-out-link" 65 | complete -c project-manager -f -l "update-input" 66 | complete -c project-manager -f -l "override-input" 67 | complete -c project-manager -f -l "experimental-features" 68 | complete -c project-manager -f -l "extra-experimental-features" 69 | complete -c project-manager -f -l "refresh" -d "Consider all previously downloaded files out-of-date" 70 | -------------------------------------------------------------------------------- /modules/lib/dag.nix: -------------------------------------------------------------------------------- 1 | # A generalization of Nixpkgs's `strings-with-deps.nix`. 2 | # 3 | # The main differences from the Nixpkgs version are 4 | # 5 | # - not specific to strings, i.e., any payload is OK, 6 | # 7 | # - the addition of the function `entryBefore` indicating a "wanted 8 | # by" relationship. 9 | {lib}: let 10 | inherit (lib) all filterAttrs head pm mapAttrs length tail toposort; 11 | in { 12 | empty = {}; 13 | 14 | isEntry = e: e ? data && e ? after && e ? before; 15 | isDag = dag: 16 | builtins.isAttrs dag && all pm.dag.isEntry (builtins.attrValues dag); 17 | 18 | # Takes an attribute set containing entries built by entryAnywhere, 19 | # entryAfter, and entryBefore to a topologically sorted list of 20 | # entries. 21 | # 22 | # Internally this function uses the `toposort` function in 23 | # `` and its value is accordingly. 24 | # 25 | # Specifically, the result on success is 26 | # 27 | # { result = [ { name = ?; data = ?; } … ] } 28 | # 29 | # For example 30 | # 31 | # nix-repl> topoSort { 32 | # a = entryAnywhere "1"; 33 | # b = entryAfter [ "a" "c" ] "2"; 34 | # c = entryBefore [ "d" ] "3"; 35 | # d = entryBefore [ "e" ] "4"; 36 | # e = entryAnywhere "5"; 37 | # } == { 38 | # result = [ 39 | # { data = "1"; name = "a"; } 40 | # { data = "3"; name = "c"; } 41 | # { data = "2"; name = "b"; } 42 | # { data = "4"; name = "d"; } 43 | # { data = "5"; name = "e"; } 44 | # ]; 45 | # } 46 | # true 47 | # 48 | # And the result on error is 49 | # 50 | # { 51 | # cycle = [ { after = ?; name = ?; data = ? } … ]; 52 | # loops = [ { after = ?; name = ?; data = ? } … ]; 53 | # } 54 | # 55 | # For example 56 | # 57 | # nix-repl> topoSort { 58 | # a = entryAnywhere "1"; 59 | # b = entryAfter [ "a" "c" ] "2"; 60 | # c = entryAfter [ "d" ] "3"; 61 | # d = entryAfter [ "b" ] "4"; 62 | # e = entryAnywhere "5"; 63 | # } == { 64 | # cycle = [ 65 | # { after = [ "a" "c" ]; data = "2"; name = "b"; } 66 | # { after = [ "d" ]; data = "3"; name = "c"; } 67 | # { after = [ "b" ]; data = "4"; name = "d"; } 68 | # ]; 69 | # loops = [ 70 | # { after = [ "a" "c" ]; data = "2"; name = "b"; } 71 | # ]; 72 | # } 73 | # true 74 | topoSort = dag: let 75 | dagBefore = dag: name: 76 | builtins.attrNames 77 | (filterAttrs (n: v: builtins.elem name v.before) dag); 78 | normalizedDag = 79 | mapAttrs (n: v: { 80 | name = n; 81 | data = v.data; 82 | after = v.after ++ dagBefore dag n; 83 | }) 84 | dag; 85 | before = a: b: builtins.elem a.name b.after; 86 | sorted = toposort before (builtins.attrValues normalizedDag); 87 | in 88 | if sorted ? result 89 | then { 90 | result = map (v: {inherit (v) name data;}) sorted.result; 91 | } 92 | else sorted; 93 | 94 | # Applies a function to each element of the given DAG. 95 | map = f: mapAttrs (n: v: v // {data = f n v.data;}); 96 | 97 | entryBetween = before: after: data: {inherit data before after;}; 98 | 99 | # Create a DAG entry with no particular dependency information. 100 | entryAnywhere = pm.dag.entryBetween [] []; 101 | 102 | entryAfter = pm.dag.entryBetween []; 103 | entryBefore = before: pm.dag.entryBetween before []; 104 | 105 | # Given a list of entries, this function places them in order within the DAG. 106 | # Each entry is labeled "${tag}-${entry index}" and other DAG entries can be 107 | # added with 'before' or 'after' referring these indexed entries. 108 | # 109 | # The entries as a whole can be given a relation to other DAG nodes. All 110 | # generated nodes are then placed before or after those dependencies. 111 | entriesBetween = tag: let 112 | go = i: before: after: entries: let 113 | name = "${tag}-${toString i}"; 114 | i' = i + 1; 115 | in 116 | if entries == [] 117 | then pm.dag.empty 118 | else if length entries == 1 119 | then { 120 | "${name}" = pm.dag.entryBetween before after (head entries); 121 | } 122 | else 123 | { 124 | "${name}" = pm.dag.entryAfter after (head entries); 125 | } 126 | // go (i + 1) before [name] (tail entries); 127 | in 128 | go 0; 129 | 130 | entriesAnywhere = tag: pm.dag.entriesBetween tag [] []; 131 | entriesAfter = tag: pm.dag.entriesBetween tag []; 132 | entriesBefore = tag: before: pm.dag.entriesBetween tag before []; 133 | } 134 | -------------------------------------------------------------------------------- /templates/default/.config/project/default.nix: -------------------------------------------------------------------------------- 1 | ### Welcome to your project configuration! 2 | ### 3 | ### This is a Nix module (https://nixos.wiki/wiki/NixOS_modules) that is 4 | ### intended to produce both flake outputs and generated files in your project. 5 | ### 6 | ### All available options for this file are listed in 7 | ### https://sellout.github.io/project-manager/options.xhtml 8 | { 9 | lib, 10 | pkgs, 11 | ... 12 | }: { 13 | project = { 14 | name = "{{project.name}}"; 15 | summary = "{{project.summary}}"; 16 | ## NB: This follows the same structure as nixpkgs maintainers, so it can 17 | ## contain references to that, like 18 | ## `authors = [lib.maintainers.sellout];`. 19 | authors = ["{{project.author}}"]; 20 | license = "{{project.license}}"; 21 | ## The `project.devPackages` option allows you to install Nix packages into 22 | ## your environment. 23 | devPackages = [ 24 | # # Adds the 'hello' command to your environment. It prints a friendly 25 | # # "Hello, world!" when run. 26 | # pkgs.hello 27 | 28 | # # It is sometimes useful to fine-tune packages, for example, by applying 29 | # # overrides. You can do that directly here, just don't forget the 30 | # # parentheses. Maybe you want to install Nerd Fonts with a limited number of 31 | # # fonts? 32 | # (pkgs.nerdfonts.override { fonts = [ "FantasqueSansMono" ]; }) 33 | 34 | # # You can also create simple shell scripts directly inside your 35 | # # configuration. For example, this adds a command 'my-hello' to your 36 | # # environment: 37 | # (pkgs.writeShellScriptBin "my-hello" '' 38 | # echo "Hello, \${config.project.username}!" 39 | # '') 40 | 41 | ## language servers 42 | pkgs.nil # Nix 43 | pkgs.nodePackages.bash-language-server 44 | ]; 45 | 46 | # Project Manager is pretty good at managing dotfiles. Many are handled 47 | # through modules for specific programs or services, but you can also create 48 | # them directly. The primary way to manage plain files is through 49 | # 'project.file'. 50 | file = { 51 | # # Building this configuration will create a copy of 'dotfiles/screenrc' in 52 | # # the Nix store. Activating the configuration will then make '~/.screenrc' a 53 | # # symlink to the Nix store copy. 54 | # ".screenrc".source = dotfiles/screenrc; 55 | 56 | # # You can also set the file content immediately. 57 | # ".gradle/gradle.properties".text = '' 58 | # org.gradle.console=verbose 59 | # org.gradle.daemon.idletimeout=3600000 60 | # ''; 61 | }; 62 | 63 | # You can also manage environment variables but you will have to manually 64 | # source 65 | # 66 | # ~/.nix-profile/etc/profile.d/pm-session-vars.sh 67 | # 68 | # or 69 | # 70 | # /etc/profiles/per-user/$USER/etc/profile.d/pm-session-vars.sh 71 | # 72 | # if you don't want to manage your shell through Project Manager. 73 | sessionVariables = { 74 | # EDITOR = "emacs"; 75 | }; 76 | 77 | # This value determines the Project Manager release that your configuration 78 | # is compatible with. This helps avoid breakage when a new Project Manager 79 | # release introduces backwards incompatible changes. 80 | # 81 | # You should not change this value, even if you update Project Manager. If 82 | # you do want to update the value, then make sure to first check the Project 83 | # Manager release notes. 84 | stateVersion = "0"; # Please read the comment before changing. 85 | }; 86 | 87 | programs = { 88 | ## Direnv is a common tool for setting up a project-specific environment 89 | ## whenever you enter a directory in your project. Project Manager can 90 | ## create the .envrc file for you. 91 | direnv = { 92 | enable = true; 93 | envrc = '' 94 | use flake 95 | ''; 96 | }; 97 | 98 | ## FIXME: This should be enabled automatically if the configuration 99 | ## determines it’s in a Git repository. Correspondingly, Mercurial or 100 | ## other VCSes should be enabled automatically if the configuration 101 | ## determines it’s in one of their repositories. 102 | git.enable = true; 103 | 104 | ## Let Project Manager install and manage itself. 105 | project-manager.enable = true; 106 | 107 | ## Treefmt is a meta-formatter – it runs other formatters for many languages 108 | ## across your repo. Enabling it makes it the formatter that Project Manager 109 | ## will set in your flake. 110 | treefmt = { 111 | enable = true; 112 | ## Turn on a Nix formatter. See 113 | ## https://github.com/numtide/treefmt-nix#supported-programs for a list of 114 | ## other supported formatters. 115 | programs.alejandra.enable = true; 116 | }; 117 | }; 118 | 119 | services = { 120 | github.enable = true; 121 | renovate.enable = true; 122 | }; 123 | } 124 | -------------------------------------------------------------------------------- /modules/services/github.nix: -------------------------------------------------------------------------------- 1 | { 2 | config, 3 | lib, 4 | pkgs, 5 | ... 6 | }: let 7 | cfg = config.services.github; 8 | 9 | projectDirectory = config.project.projectDirectory; 10 | 11 | fileType = 12 | (import ../lib/file-type.nix { 13 | inherit projectDirectory lib pkgs; 14 | commit-by-default = config.project.commit-by-default; 15 | }) 16 | .fileType; 17 | in { 18 | meta.maintainers = [lib.maintainers.sellout]; 19 | 20 | options.services.github = { 21 | enable = lib.mkEnableOption "GitHub"; 22 | 23 | settings = lib.mkOption { 24 | description = '' 25 | Declarative GitHub settings, as provided by [Probot’s Settings 26 | app](https://probot.github.io/apps/settings/). 27 | ''; 28 | type = lib.types.nullOr (lib.types.submodule { 29 | freeformType = lib.types.attrs; 30 | options = { 31 | branches = lib.mkOption { 32 | type = lib.types.attrsOf (lib.types.attrsOf lib.types.anything); 33 | default = {}; 34 | description = '' 35 | Labels for issues & PRs. 36 | ''; 37 | }; 38 | labels = lib.mkOption { 39 | type = lib.types.attrsOf (lib.types.attrsOf lib.types.anything); 40 | default = {}; 41 | description = '' 42 | Branch-specific settings. 43 | ''; 44 | }; 45 | repository = lib.mkOption { 46 | description = '' 47 | Settings that affect the entire repository. 48 | ''; 49 | type = lib.types.submodule { 50 | freeformType = lib.types.anything; 51 | options.topics = lib.mkOption { 52 | default = []; 53 | type = lib.types.listOf lib.types.str; 54 | description = '' 55 | A list of GitHub “topics”. 56 | ''; 57 | }; 58 | }; 59 | }; 60 | }; 61 | }); 62 | default = null; 63 | }; 64 | 65 | workflow = lib.mkOption { 66 | description = '' 67 | Attribute set of GitHub workflows. 68 | ''; 69 | default = {}; 70 | type = 71 | fileType 72 | "services.github.workflow" 73 | "" 74 | (projectDirectory + "/.github/workflows"); 75 | }; 76 | }; 77 | 78 | config = lib.mkIf cfg.enable (let 79 | ## Have users provide a list of topics, then convert it into the comma- 80 | ## separated string expected by settings.yml. 81 | concatTopics = settings: 82 | if settings ? repository && settings.repository ? topics 83 | then 84 | settings 85 | // { 86 | repository = 87 | settings.repository 88 | // {topics = lib.concatStringsSep ", " settings.repository.topics;}; 89 | } 90 | else settings; 91 | 92 | ## Coverts a structure like `{foo = {x = y;}; bar = {x = z;};}` to one like 93 | ## `[{name = "foo"; x = y;} {name = "bar"; x = z;}]`. 94 | attrsToNamedList = lib.mapAttrsToList (k: v: 95 | if v ? name 96 | then v 97 | else v // {name = k;}); 98 | 99 | ## settings.yaml expects a list of branches, but that doesn’t have the most 100 | ## useful merge semantics. So we have an attrSet of branches, with the key 101 | ## being used as the name, if the name isn’t otherwise set. 102 | restructureBranches = settings: 103 | if settings ? branches 104 | then settings // {branches = attrsToNamedList settings.branches;} 105 | else settings; 106 | 107 | restructureLabels = settings: 108 | if settings ? labels 109 | then settings // {labels = attrsToNamedList settings.labels;} 110 | else settings; 111 | 112 | generatedAndCommitted = 113 | lib.filterAttrs (n: v: v.persistence == "repository") config.project.file; 114 | in { 115 | programs.git = { 116 | ## TODO: This probably isn’t right – just because the user users git 117 | ## doesn’t mean they want it managed by Nix – but we _do_ want to 118 | ## manage .gitattributes here. Maybe we can just cause the text to 119 | ## be appended? 120 | enable = true; 121 | 122 | attributes = 123 | lib.mapAttrs' 124 | (_: v: { 125 | name = "/" + v.target; 126 | value.linguist-generated = true; 127 | }) 128 | generatedAndCommitted; 129 | }; 130 | 131 | project.file = 132 | { 133 | ## This should always have at least one `linguist-generated` entry (for 134 | ## .gitattributes itself), so we always commit. 135 | ".gitattributes".commit-by-default = true; 136 | ".github/settings.yml" = 137 | lib.mkIf (cfg.settings != null) 138 | { 139 | text = 140 | lib.pm.generators.toYAML {} 141 | (restructureBranches (restructureLabels (concatTopics cfg.settings))); 142 | }; 143 | } 144 | // cfg.workflow; 145 | }); 146 | } 147 | -------------------------------------------------------------------------------- /modules/lib/types/default.nix: -------------------------------------------------------------------------------- 1 | { 2 | lib, 3 | gvariant ? import ../gvariant.nix {inherit lib;}, 4 | }: let 5 | inherit 6 | (lib) 7 | all 8 | concatMap 9 | foldl' 10 | getFiles 11 | getValues 12 | head 13 | isFunction 14 | literalExpression 15 | mergeAttrs 16 | mergeDefaultOption 17 | mergeOneOption 18 | mkOption 19 | mkOptionType 20 | showFiles 21 | showOption 22 | types 23 | ; 24 | 25 | typesDag = import ./dag.nix {inherit lib;}; 26 | 27 | # Needed since the type is called gvariant and its merge attribute 28 | # must refer back to the type. 29 | gvar = gvariant; 30 | in rec { 31 | inherit (typesDag) dagOf; 32 | 33 | ## A list of “[globs](https://en.wikipedia.org/wiki/Glob_(programming))”. It 34 | ## takes a predicate that returns `true` if a value of this type matches every 35 | ## possible value (a “universal match”, e.g., a list like `["*"]` for standard 36 | ## Unix-like globs). Universal matches are handled specially. They replace any 37 | ## definition that has a lower priority than them, but are replaced by all 38 | ## definitions that have a higher priority than them. 39 | globList = isUniversal: 40 | lib.types.listOf lib.types.str 41 | // { 42 | merge = _loc: defs: 43 | lib.foldr (def: prev: 44 | if isUniversal def || isUniversal prev 45 | then def 46 | else def ++ prev) 47 | [] 48 | (lib.getValues defs); 49 | }; 50 | 51 | selectorFunction = mkOptionType { 52 | name = "selectorFunction"; 53 | description = 54 | "Function that takes an attribute set and returns a list" 55 | + " containing a selection of the values of the input set"; 56 | check = isFunction; 57 | merge = _loc: defs: as: concatMap (select: select as) (getValues defs); 58 | }; 59 | 60 | overlayFunction = mkOptionType { 61 | name = "overlayFunction"; 62 | description = 63 | "An overlay function, takes self and super and returns" 64 | + " an attribute set overriding the desired attributes."; 65 | check = isFunction; 66 | merge = _loc: defs: self: super: 67 | foldl' (res: def: mergeAttrs res (def.value self super)) {} defs; 68 | }; 69 | 70 | fontType = types.submodule { 71 | options = { 72 | package = mkOption { 73 | type = types.nullOr types.package; 74 | default = null; 75 | example = literalExpression "pkgs.dejavu_fonts"; 76 | description = '' 77 | Package providing the font. This package will be installed 78 | to your profile. If `null` then the font 79 | is assumed to already be available in your profile. 80 | ''; 81 | }; 82 | 83 | name = mkOption { 84 | type = types.str; 85 | example = "DejaVu Sans"; 86 | description = '' 87 | The family name of the font within the package. 88 | ''; 89 | }; 90 | 91 | size = mkOption { 92 | type = types.nullOr types.number; 93 | default = null; 94 | example = "8"; 95 | description = '' 96 | The size of the font. 97 | ''; 98 | }; 99 | }; 100 | }; 101 | 102 | gvariant = mkOptionType rec { 103 | name = "gvariant"; 104 | description = "GVariant value"; 105 | check = v: gvar.mkValue v != null; 106 | merge = loc: defs: let 107 | vdefs = map (d: 108 | d 109 | // { 110 | value = 111 | if gvar.isGVariant d.value 112 | then d.value 113 | else gvar.mkValue d.value; 114 | }) 115 | defs; 116 | vals = map (d: d.value) vdefs; 117 | defTypes = map (x: x.type) vals; 118 | sameOrNull = x: y: 119 | if x == y 120 | then y 121 | else null; 122 | # A bit naive to just check the first entry… 123 | sharedDefType = foldl' sameOrNull (head defTypes) defTypes; 124 | allChecked = all (x: check x) vals; 125 | in 126 | if sharedDefType == null 127 | then 128 | throw ("Cannot merge definitions of `${showOption loc}' with" 129 | + " mismatched GVariant types given in" 130 | + " ${showFiles (getFiles defs)}.") 131 | else if gvar.isArray sharedDefType && allChecked 132 | then 133 | gvar.mkValue ((types.listOf gvariant).merge loc 134 | (map (d: d // {value = d.value.value;}) vdefs)) 135 | // { 136 | type = sharedDefType; 137 | } 138 | else if gvar.isTuple sharedDefType && allChecked 139 | then mergeOneOption loc defs 140 | else if gvar.isMaybe sharedDefType && allChecked 141 | then mergeOneOption loc defs 142 | else if gvar.isDictionaryEntry sharedDefType && allChecked 143 | then mergeOneOption loc defs 144 | else if gvar.type.variant == sharedDefType && allChecked 145 | then mergeOneOption loc defs 146 | else if gvar.type.string == sharedDefType && allChecked 147 | then types.str.merge loc defs 148 | else if gvar.type.double == sharedDefType && allChecked 149 | then types.float.merge loc defs 150 | else mergeDefaultOption loc defs; 151 | }; 152 | } 153 | -------------------------------------------------------------------------------- /modules/lib/gvariant.nix: -------------------------------------------------------------------------------- 1 | # A partial and basic implementation of GVariant formatted strings. 2 | # 3 | # Note, this API is not considered fully stable and it might therefore 4 | # change in backwards incompatible ways without prior notice. 5 | {lib}: let 6 | inherit 7 | (lib) 8 | concatMapStringsSep 9 | concatStrings 10 | escape 11 | hasPrefix 12 | head 13 | replaceStrings 14 | ; 15 | 16 | mkPrimitive = t: v: { 17 | _type = "gvariant"; 18 | type = t; 19 | value = v; 20 | __toString = self: "@${self.type} ${toString self.value}"; 21 | }; 22 | 23 | type = { 24 | arrayOf = t: "a${t}"; 25 | maybeOf = t: "m${t}"; 26 | tupleOf = ts: "(${concatStrings ts})"; 27 | dictionaryEntryOf = ts: "{${concatStrings ts}}"; 28 | string = "s"; 29 | boolean = "b"; 30 | uchar = "y"; 31 | int16 = "n"; 32 | uint16 = "q"; 33 | int32 = "i"; 34 | uint32 = "u"; 35 | int64 = "x"; 36 | uint64 = "t"; 37 | double = "d"; 38 | variant = "v"; 39 | }; 40 | 41 | # Returns the GVariant type of a given Nix value. If no type can be 42 | # found for the value then the empty string is returned. 43 | typeOf = v: 44 | with type; 45 | if builtins.isBool v 46 | then boolean 47 | else if builtins.isInt v 48 | then int32 49 | else if builtins.isFloat v 50 | then double 51 | else if builtins.isString v 52 | then string 53 | else if builtins.isList v 54 | then let 55 | elemType = elemTypeOf v; 56 | in 57 | if elemType == "" 58 | then "" 59 | else arrayOf elemType 60 | else if builtins.isAttrs v && v ? type 61 | then v.type 62 | else ""; 63 | 64 | elemTypeOf = vs: 65 | if builtins.isList vs 66 | then 67 | if vs == [] 68 | then "" 69 | else typeOf (head vs) 70 | else ""; 71 | 72 | mkMaybe = elemType: elem: 73 | mkPrimitive (type.maybeOf elemType) elem 74 | // { 75 | __toString = self: 76 | if self.value == null 77 | then "@${self.type} nothing" 78 | else "just ${toString self.value}"; 79 | }; 80 | in rec { 81 | inherit type typeOf; 82 | 83 | isGVariant = v: v._type or "" == "gvariant"; 84 | 85 | isArray = hasPrefix "a"; 86 | isDictionaryEntry = hasPrefix "{"; 87 | isMaybe = hasPrefix "m"; 88 | isTuple = hasPrefix "("; 89 | 90 | # Returns the GVariant value that most closely matches the given Nix 91 | # value. If no GVariant value can be found then `null` is returned. 92 | 93 | mkValue = v: 94 | if builtins.isBool v 95 | then mkBoolean v 96 | else if builtins.isInt v 97 | then mkInt32 v 98 | else if builtins.isFloat v 99 | then mkDouble v 100 | else if builtins.isString v 101 | then mkString v 102 | else if builtins.isList v 103 | then 104 | if v == [] 105 | then mkArray type.string [] 106 | else mkArray (elemTypeOf v) v 107 | else if builtins.isAttrs v && (v._type or "") == "gvariant" 108 | then v 109 | else null; 110 | 111 | mkArray = elemType: elems: 112 | mkPrimitive (type.arrayOf elemType) (map mkValue elems) 113 | // { 114 | __toString = self: "@${self.type} [${concatMapStringsSep "," toString self.value}]"; 115 | }; 116 | 117 | mkEmptyArray = elemType: mkArray elemType []; 118 | 119 | mkVariant = elem: let 120 | gvarElem = mkValue elem; 121 | in 122 | mkPrimitive type.variant gvarElem 123 | // { 124 | __toString = self: "@${self.type} <${toString self.value}>"; 125 | }; 126 | 127 | mkDictionaryEntry = elems: let 128 | gvarElems = map mkValue elems; 129 | dictionaryType = type.dictionaryEntryOf (map (e: e.type) gvarElems); 130 | in 131 | mkPrimitive dictionaryType gvarElems 132 | // { 133 | __toString = self: "@${self.type} {${concatMapStringsSep "," toString self.value}}"; 134 | }; 135 | 136 | mkNothing = elemType: mkMaybe elemType null; 137 | 138 | mkJust = elem: let gvarElem = mkValue elem; in mkMaybe gvarElem.type gvarElem; 139 | 140 | mkTuple = elems: let 141 | gvarElems = map mkValue elems; 142 | tupleType = type.tupleOf (map (e: e.type) gvarElems); 143 | in 144 | mkPrimitive tupleType gvarElems 145 | // { 146 | __toString = self: "@${self.type} (${concatMapStringsSep "," toString self.value})"; 147 | }; 148 | 149 | mkBoolean = v: 150 | mkPrimitive type.boolean v 151 | // { 152 | __toString = self: 153 | if self.value 154 | then "true" 155 | else "false"; 156 | }; 157 | 158 | mkString = v: let 159 | sanitize = s: replaceStrings ["\n"] ["\\n"] (escape ["'" "\\"] s); 160 | in 161 | mkPrimitive type.string v 162 | // { 163 | __toString = self: "'${sanitize self.value}'"; 164 | }; 165 | 166 | mkObjectpath = v: 167 | mkPrimitive type.string v 168 | // { 169 | __toString = self: "objectpath '${escape ["'"] self.value}'"; 170 | }; 171 | 172 | mkUchar = mkPrimitive type.uchar; 173 | 174 | mkInt16 = mkPrimitive type.int16; 175 | 176 | mkUint16 = mkPrimitive type.uint16; 177 | 178 | mkInt32 = v: 179 | mkPrimitive type.int32 v 180 | // { 181 | __toString = self: toString self.value; 182 | }; 183 | 184 | mkUint32 = mkPrimitive type.uint32; 185 | 186 | mkInt64 = mkPrimitive type.int64; 187 | 188 | mkUint64 = mkPrimitive type.uint64; 189 | 190 | mkDouble = v: 191 | mkPrimitive type.double v 192 | // { 193 | __toString = self: toString self.value; 194 | }; 195 | } 196 | -------------------------------------------------------------------------------- /testing/flake.nix: -------------------------------------------------------------------------------- 1 | # This is an internal Nix Flake intended for use when running tests. 2 | # 3 | # You can build all tests or specific tests by running 4 | # 5 | # nix build --reference-lock-file flake.lock ./testing#test-all 6 | # nix build --reference-lock-file flake.lock ./testing#test-alacritty-empty-settings 7 | # 8 | # in the Home Manager project root directory. 9 | # 10 | # Similarly for integration tests 11 | # 12 | # nix build --reference-lock-file flake.lock ./testing#integration-test-all 13 | # nix build --reference-lock-file flake.lock ./testing#integration-test-standalone-standard-basics 14 | { 15 | description = "Tests of Home Manager for Nix"; 16 | 17 | outputs = { 18 | flaky, 19 | nixpkgs, 20 | self, 21 | systems, 22 | }: let 23 | supportedSystems = import systems; 24 | forAllSystems = nixpkgs.lib.genAttrs supportedSystems; 25 | forCI = nixpkgs.lib.genAttrs supportedSystems; 26 | 27 | testChunks = system: let 28 | pkgs = nixpkgs.legacyPackages.${system}; 29 | inherit (pkgs) lib; 30 | 31 | # Create chunked test packages for better CI parallelization 32 | tests = import ./. { 33 | inherit pkgs; 34 | pmPkgs = pkgs.appendOverlays [flaky.overlays.default]; 35 | # Disable big tests since this is only used for CI 36 | enableBig = false; 37 | }; 38 | allTests = lib.attrNames tests.build; 39 | # Remove 'all' from the test list as it's a meta-package 40 | filteredTests = lib.filter (name: name != "all") allTests; 41 | # NOTE: Just a starting value, we can tweak this to find a good value. 42 | targetTestsPerChunk = 50; 43 | numChunks = lib.max 1 ( 44 | builtins.ceil ((builtins.length filteredTests) / (targetTestsPerChunk * 1.0)) 45 | ); 46 | chunkSize = builtins.ceil ((builtins.length filteredTests) / (numChunks * 1.0)); 47 | 48 | makeChunk = chunkNum: testList: let 49 | start = (chunkNum - 1) * chunkSize; 50 | end = lib.min (start + chunkSize) (builtins.length testList); 51 | chunkTests = lib.sublist start (end - start) testList; 52 | chunkAttrs = lib.genAttrs chunkTests (name: tests.build.${name}); 53 | in 54 | pkgs.symlinkJoin { 55 | name = "test-chunk-${toString chunkNum}"; 56 | paths = lib.attrValues chunkAttrs; 57 | passthru.tests = chunkTests; 58 | }; 59 | in 60 | lib.listToAttrs ( 61 | lib.genList ( 62 | i: lib.nameValuePair "test-chunk-${toString (i + 1)}" (makeChunk (i + 1) filteredTests) 63 | ) 64 | numChunks 65 | ); 66 | 67 | integrationTests = system: let 68 | pkgs = nixpkgs.legacyPackages.${system}; 69 | inherit (pkgs) lib; 70 | in 71 | lib.optionalAttrs pkgs.stdenv.hostPlatform.isLinux ( 72 | # let 73 | # tests = import ./integration { inherit pkgs lib; }; 74 | # renameTestPkg = n: v: lib.nameValuePair "integration-${n}" v; 75 | # in 76 | # lib.mapAttrs' renameTestPkg (lib.removeAttrs tests [ "all" ]) 77 | {} 78 | ); 79 | 80 | # Test group definitions 81 | buildTests = system: let 82 | pkgs = nixpkgs.legacyPackages.${system}; 83 | tests = import ./. { 84 | inherit pkgs; 85 | pmPkgs = pkgs.appendOverlays [flaky.overlays.default]; 86 | }; 87 | renameTestPkg = n: nixpkgs.lib.nameValuePair "test-${n}"; 88 | in 89 | nixpkgs.lib.mapAttrs' renameTestPkg tests.build; 90 | 91 | buildTestsNoBig = system: let 92 | pkgs = nixpkgs.legacyPackages.${system}; 93 | tests = import ./. { 94 | inherit pkgs; 95 | pmPkgs = pkgs.appendOverlays [flaky.overlays.default]; 96 | enableBig = false; 97 | }; 98 | in { 99 | test-all-no-big = tests.build.all; 100 | }; 101 | 102 | buildTestsNoBigIfd = system: let 103 | pkgs = nixpkgs.legacyPackages.${system}; 104 | tests = import ./. { 105 | inherit pkgs; 106 | pmPkgs = pkgs.appendOverlays [flaky.overlays.default]; 107 | enableBig = false; 108 | }; 109 | in { 110 | test-all-no-big-ifd = tests.build.all; 111 | }; 112 | 113 | integrationTestPackages = system: 114 | # let 115 | # pkgs = nixpkgs.legacyPackages.${system}; 116 | # inherit (pkgs) lib; 117 | # tests = import ./integration { inherit pkgs lib; }; 118 | # renameTestPkg = n: lib.nameValuePair "integration-test-${n}"; 119 | # in 120 | # lib.mapAttrs' renameTestPkg tests; 121 | {}; 122 | in { 123 | # TODO: increase buildbot testing scope 124 | buildbot = forCI ( 125 | system: let 126 | allIntegrationTests = integrationTests system; 127 | workingIntegrationTests = 128 | nixpkgs.lib.filterAttrs ( 129 | name: _: 130 | nixpkgs.lib.elem name [ 131 | "integration-nixos-basics" 132 | "integration-nixos-legacy-profile-management" 133 | ] 134 | ) 135 | allIntegrationTests; 136 | in 137 | (testChunks system) // workingIntegrationTests 138 | ); 139 | 140 | devShells = forAllSystems ( 141 | system: let 142 | pkgs = nixpkgs.legacyPackages.${system}; 143 | tests = import ./. { 144 | inherit pkgs; 145 | pmPkgs = pkgs.appendOverlays [flaky.overlays.default]; 146 | }; 147 | in 148 | tests.run 149 | ); 150 | 151 | packages = forAllSystems ( 152 | system: 153 | (buildTests system) 154 | // (integrationTestPackages system) 155 | // (buildTestsNoBig system) 156 | // (buildTestsNoBigIfd system) 157 | // (testChunks system) 158 | // (integrationTests system) 159 | ); 160 | }; 161 | 162 | inputs = { 163 | flaky.url = "github:sellout/flaky"; 164 | 165 | nixpkgs.follows = "flaky/nixpkgs"; 166 | systems.follows = "flaky/systems"; 167 | }; 168 | } 169 | -------------------------------------------------------------------------------- /modules/services/nix-ci.nix: -------------------------------------------------------------------------------- 1 | ## TODO: Ideally this would live in some Nix CI flake, produced via the original 2 | ## codec, which I would imagine is defined by Autodocodec somewhere, but 3 | ## for now, it’s manualy. 4 | { 5 | config, 6 | lib, 7 | self, 8 | ... 9 | }: let 10 | cfg = config.services.nix-ci; 11 | in { 12 | options.services.nix-ci = let 13 | t = lib.types; 14 | in { 15 | ## FIXME: Try to recover my deleted version of this from Time Machine 16 | enable = lib.mkOption { 17 | type = t.nullOr t.bool; 18 | default = null; 19 | example = false; 20 | description = '' 21 | [Nix CI](https://nix-ci.com/) is enabled via 22 | [GitHub](https://github.com/). This option controls the (optional) 23 | nix-ci.nix file that it uses for configuration. The default (`null`) is 24 | to not generate a file at all. If explicitly set to `false`, it will 25 | generate the file, but set `enable = false` in the file, which 26 | _disables_ Nix CI, even if it’s enabled via GitHub. This is particularly 27 | useful if you have Nix CI enabled for your entire org, but want to 28 | disable it on some repositories. 29 | ''; 30 | }; 31 | 32 | systems = lib.mkOption { 33 | type = t.nullOr (t.listOf t.str); 34 | default = null; 35 | description = '' 36 | Only build for these systems. By default these are computed based on 37 | which workers are available for the repository. 38 | ''; 39 | }; 40 | 41 | onlyBuild = lib.mkOption { 42 | type = t.nullOr (t.listOf t.str); 43 | default = null; 44 | description = '' 45 | Only build these attributes. 46 | ''; 47 | }; 48 | 49 | doNotBuild = lib.mkOption { 50 | type = t.listOf t.str; 51 | default = []; 52 | description = '' 53 | Exclude these attributes. 54 | ''; 55 | }; 56 | 57 | git-ssh-key = lib.mkOption { 58 | type = t.nullOr (t.attrsOf t.str); 59 | default = null; 60 | description = '' 61 | SSH key to use for [Git](https://git-scm.com/) cloning. 62 | 63 | By default no SSH key is used so Nix will fail to clone private 64 | dependencies. 65 | 66 | See [the relevant Nix CI 67 | documentation](https://nix-ci.com/documentation/git-ssh-key) for more. 68 | ''; 69 | }; 70 | 71 | timeout = lib.mkOption { 72 | type = t.nullOr t.numbers.nonnegative; 73 | default = null; 74 | description = '' 75 | Maximum timeout, in seconds. Note that the actual timeout may depend on 76 | the workers’ configuration. 77 | ''; 78 | }; 79 | 80 | cache = lib.mkOption { 81 | type = t.nullOr (t.attrsOf t.str); 82 | default = null; 83 | description = '' 84 | SSH cache configuration. 85 | 86 | In order to push to the cache as well, the repository needs to have an 87 | `SSH_CACHE_PRIVATE_KEY` secret. 88 | ''; 89 | }; 90 | 91 | cachix = lib.mkOption { 92 | type = t.nullOr (t.attrsOf t.str); 93 | default = null; 94 | description = '' 95 | [Cachix](https://www.cachix.org/) configuration. 96 | 97 | In order to push to the cache as well, the repository needs to have a 98 | `CACHIX_AUTH_TOKEN` or `CACHIX_SIGNING_KEY` secret. 99 | 100 | See [the relevant Nix CI 101 | documentation](https://nix-ci.com/documentation/cachix) for more. 102 | ''; 103 | }; 104 | 105 | allow-import-from-derivation = lib.mkOption { 106 | type = t.bool; 107 | default = true; 108 | example = false; 109 | description = '' 110 | Show with `--allow-import-from-derivation`. 111 | ''; 112 | }; 113 | 114 | impure = lib.mkOption { 115 | type = t.bool; 116 | default = false; 117 | example = true; 118 | description = '' 119 | Build with `--impure`. 120 | ''; 121 | }; 122 | 123 | build-logs = lib.mkOption { 124 | type = t.bool; 125 | default = true; 126 | example = false; 127 | description = '' 128 | Build with `--print-build-logs`. 129 | ''; 130 | }; 131 | 132 | fail-fast = lib.mkOption { 133 | type = t.bool; 134 | default = true; 135 | example = false; 136 | description = '' 137 | Cancel the rest of a suite once one job fails. 138 | ''; 139 | }; 140 | 141 | auto-retry = lib.mkOption { 142 | type = t.bool; 143 | default = true; 144 | example = false; 145 | description = '' 146 | Automatically retry every individual failed run in a suite once. 147 | ''; 148 | }; 149 | 150 | test = lib.mkOption { 151 | type = t.attrsOf t.attrs; 152 | default = {}; 153 | description = '' 154 | Test configurations. 155 | 156 | See [the relevant Nix CI 157 | documentation](https://nix-ci.com/documentation/test) for more. 158 | ''; 159 | }; 160 | 161 | deploy = lib.mkOption { 162 | type = t.attrsOf t.attrs; 163 | default = {}; 164 | description = '' 165 | Deploy configurations. 166 | 167 | See [the relevant Nix CI 168 | documentation](https://nix-ci.com/documentation/deploy) for more. 169 | ''; 170 | }; 171 | }; 172 | 173 | config = lib.mkIf (cfg.enable != null) { 174 | project.file."nix-ci.nix".text = 175 | lib.pm.generators.toPretty {multiline = false;} 176 | ## TODO: Elide the other default values for a smaller/cleaner file. 177 | (lib.filterAttrs (_: v: v != null) cfg); 178 | 179 | ## TODO: This duplicates logic in garnix.nix. The common functionality 180 | ## should be extracted, and maybe located near where 181 | ## `unsandboxedChecks` or `lax-checks` are defined. 182 | services.nix-ci.doNotBuild = lib.concatMap (sys: 183 | map (name: "checks.${sys}.${name}") 184 | (builtins.attrNames self.projectConfigurations.${sys}.unsandboxedChecks) 185 | ++ ["devShells.${sys}.lax-checks"]) 186 | ["x86_64-linux"]; 187 | }; 188 | } 189 | -------------------------------------------------------------------------------- /testing/default.nix: -------------------------------------------------------------------------------- 1 | { 2 | pkgs ? import {}, 3 | pmPkgs, 4 | enableBig ? true, 5 | }: let 6 | lib = import ../modules/lib/stdlib-extended.nix pkgs.lib; 7 | 8 | releaseInfo = import ../release.nix; 9 | 10 | nmtSrc = fetchTarball { 11 | url = "https://git.sr.ht/~rycee/nmt/archive/v0.5.1.tar.gz"; 12 | sha256 = "0qhn7nnwdwzh910ss78ga2d00v42b0lspfd7ybl61mpfgz3lmdcj"; 13 | }; 14 | 15 | # Recursively replace each derivation in the given attribute set with the same 16 | # derivation but with the `outPath` attribute set to the string 17 | # `"@package-name@"`. This allows the tests to refer to derivations through 18 | # their values without establishing an actual dependency on the derivation 19 | # output. 20 | scrubDerivation = name: value: let 21 | scrubbedValue = scrubDerivations value; 22 | 23 | newDrvAttrs = { 24 | buildScript = abort "no build allowed"; 25 | 26 | outPath = builtins.traceVerbose "${name} - got out path" "@${lib.getName value}@"; 27 | 28 | # Prevent getOutput from descending into outputs 29 | outputSpecified = true; 30 | 31 | # Allow the original package to be used in derivation inputs 32 | __spliced = { 33 | buildHost = value; 34 | hostTarget = value; 35 | }; 36 | }; 37 | in 38 | if lib.isAttrs value 39 | then 40 | if lib.isDerivation value 41 | then scrubbedValue // newDrvAttrs 42 | else scrubbedValue 43 | else value; 44 | scrubDerivations = attrs: lib.mapAttrs scrubDerivation attrs; 45 | 46 | # Globally unscrub a few selected packages that are used by a wide selection of tests. 47 | whitelist = let 48 | inner = self: super: { 49 | inherit 50 | (pkgs) 51 | coreutils 52 | jq 53 | desktop-file-utils 54 | diffutils 55 | findutils 56 | glibcLocales 57 | gettext 58 | gnugrep 59 | gnused 60 | shared-mime-info 61 | emptyDirectory 62 | # Needed by pretty much all tests that have anything to do with fish. 63 | babelfish 64 | fish 65 | ; 66 | 67 | xorg = super.xorg.overrideScope (self: super: {inherit (pkgs.xorg) lndir;}); 68 | }; 69 | 70 | outer = self: super: 71 | inner self super 72 | // { 73 | buildPackages = super.buildPackages.extend inner; 74 | }; 75 | in 76 | outer; 77 | 78 | # TODO: figure out stdenv stubbing so we don't have to do this 79 | darwinScrublist = import ./darwinScrublist.nix {inherit lib scrubDerivation;}; 80 | 81 | scrubbedPkgs = 82 | # TODO: fix darwin stdenv stubbing 83 | if isDarwin 84 | then let 85 | rawPkgs = lib.makeExtensible (final: pkgs); 86 | in 87 | builtins.traceVerbose "eval scrubbed darwin nixpkgs" (rawPkgs.extend darwinScrublist) 88 | else let 89 | rawScrubbedPkgs = lib.makeExtensible (final: scrubDerivations pkgs); 90 | in 91 | builtins.traceVerbose "eval scrubbed nixpkgs" (rawScrubbedPkgs.extend whitelist); 92 | 93 | modules = 94 | import ../modules/all-modules.nix { 95 | inherit lib pkgs; 96 | check = false; 97 | modules = lib.attrValues (import ../modules/modules.nix); 98 | } 99 | ++ [ 100 | ( 101 | {config, ...}: { 102 | _module.args = { 103 | inherit pmPkgs; 104 | # Prevent the nixpkgs module from working. We want to minimize the number 105 | # of evaluations of Nixpkgs. 106 | pkgsPath = abort "pkgs path is unavailable in tests"; 107 | realPkgs = pkgs; 108 | pkgs = let 109 | overlays = 110 | config.test.stubOverlays; 111 | # ++ lib.optionals ( 112 | # config.nixpkgs.overlays != null && config.nixpkgs.overlays != [ ] 113 | # ) config.nixpkgs.overlays; 114 | stubbedPkgs = 115 | if overlays == [] 116 | then scrubbedPkgs 117 | else builtins.traceVerbose "eval overlayed nixpkgs" (lib.foldr (o: p: p.extend o) scrubbedPkgs overlays); 118 | in 119 | lib.mkImageMediaOverride stubbedPkgs; 120 | }; 121 | 122 | ## The minimal configuration needed by a project. 123 | project = { 124 | name = lib.mkDefault "test"; 125 | ## By default, tests run with the current `stateVersion`. It can be 126 | ## set in tests to ensure the behavior is consistent with previous 127 | ## states. 128 | # stateVersion = lib.mkDefault releaseInfo.version.major; 129 | }; 130 | 131 | # Avoid including documentation since this will cause 132 | # unnecessary rebuilds of the tests. 133 | manual.manpages.enable = lib.mkDefault false; 134 | 135 | imports = [ 136 | ./asserts.nix 137 | ./big-test.nix 138 | ./stubs.nix 139 | ]; 140 | 141 | test.enableBig = enableBig; 142 | } 143 | ) 144 | ]; 145 | 146 | isDarwin = pkgs.stdenv.hostPlatform.isDarwin; 147 | isLinux = pkgs.stdenv.hostPlatform.isLinux; 148 | in 149 | import nmtSrc { 150 | inherit lib pkgs modules; 151 | testedAttrPath = ["project" "packages" "activation"]; 152 | tests = 153 | builtins.foldl' 154 | ( 155 | a: b: 156 | a 157 | // ( 158 | let 159 | imported = import (lib.path.append b "tests"); 160 | in 161 | if lib.isFunction imported 162 | then imported {inherit lib pkgs;} 163 | else imported 164 | ) 165 | ) 166 | {} 167 | ( 168 | [ 169 | # keep-sorted start 170 | ../modules/files 171 | # ../modules/lib/generators 172 | ../modules/lib/types 173 | # ../modules/project-environment 174 | # keep-sorted end 175 | ] 176 | ++ ( 177 | lib.concatMap 178 | ( 179 | dir: 180 | lib.pipe dir [ 181 | builtins.readDir 182 | (lib.filterAttrs (_path: kind: kind == "directory")) 183 | (lib.mapAttrsToList (path: _kind: lib.path.append dir path)) 184 | ] 185 | ) 186 | [ 187 | # keep-sorted start 188 | ../modules/misc 189 | ../modules/programs 190 | ../modules/services 191 | # keep-sorted end 192 | ] 193 | ) 194 | ); 195 | } 196 | -------------------------------------------------------------------------------- /docs/default.nix: -------------------------------------------------------------------------------- 1 | { 2 | pkgs, 3 | # Note, this should be "the standard library" + PM extensions. 4 | lib ? import ../modules/lib/stdlib-extended.nix pkgs.lib, 5 | self, 6 | release, 7 | isReleaseBranch, 8 | }: let 9 | # Recursively replace each derivation in the given attribute set 10 | # with the same derivation but with the `outPath` attribute set to 11 | # the string `"\${pkgs.attribute.path}"`. This allows the 12 | # documentation to refer to derivations through their values without 13 | # establishing an actual dependency on the derivation output. 14 | # 15 | # This is not perfect, but it seems to cover a vast majority of use 16 | # cases. 17 | # 18 | # Caveat: even if the package is reached by a different means, the 19 | # path above will be shown and not e.g. 20 | # `${config.services.foo.package}`. 21 | scrubDerivations = prefixPath: attrs: let 22 | scrubDerivation = name: value: let 23 | pkgAttrName = prefixPath + "." + name; 24 | in 25 | if lib.isAttrs value 26 | then 27 | scrubDerivations pkgAttrName value 28 | // lib.optionalAttrs (lib.isDerivation value) { 29 | outPath = "\${${pkgAttrName}}"; 30 | } 31 | else value; 32 | in 33 | lib.mapAttrs scrubDerivation attrs; 34 | 35 | # Make sure the used package is scrubbed to avoid actually 36 | # instantiating derivations. 37 | scrubbedPkgsModule = { 38 | imports = [ 39 | { 40 | _module.args = { 41 | pkgs = lib.mkForce (scrubDerivations "pkgs" pkgs); 42 | pkgs_i686 = lib.mkForce {}; 43 | }; 44 | } 45 | ]; 46 | }; 47 | 48 | dontCheckDefinitions = {_module.check = false;}; 49 | 50 | gitHubDeclaration = user: repo: urlRef: subpath: { 51 | url = "https://github.com/${user}/${repo}/blob/${urlRef}/${subpath}"; 52 | name = "<${repo}/${subpath}>"; 53 | }; 54 | 55 | pmPath = toString ./..; 56 | 57 | buildOptionsDocs = args @ { 58 | modules, 59 | includeModuleSystemOptions ? true, 60 | ... 61 | }: let 62 | options = 63 | (lib.evalModules { 64 | inherit modules; 65 | class = "projectManager"; 66 | }) 67 | .options; 68 | in 69 | pkgs.buildPackages.nixosOptionsDoc ({ 70 | options = 71 | if includeModuleSystemOptions 72 | then options 73 | else builtins.removeAttrs options ["_module"]; 74 | transformOptions = opt: 75 | opt 76 | // { 77 | # Clean up declaration sites to not refer to the Project Manager 78 | # source tree. 79 | declarations = map (decl: 80 | if lib.hasPrefix pmPath (toString decl) 81 | then 82 | gitHubDeclaration "sellout" "project-manager" 83 | ( 84 | if isReleaseBranch 85 | then "v${release}" 86 | else "master" 87 | ) 88 | (lib.removePrefix "/" (lib.removePrefix pmPath (toString decl))) 89 | else if decl == "lib/modules.nix" 90 | then 91 | # TODO: handle this in a better way (may require upstream 92 | # changes to nixpkgs) 93 | gitHubDeclaration "NixOS" "nixpkgs" "master" decl 94 | else decl) 95 | opt.declarations; 96 | }; 97 | } 98 | // builtins.removeAttrs args ["modules" "includeModuleSystemOptions"]); 99 | 100 | pmOptionsDocs = buildOptionsDocs { 101 | modules = 102 | import ../modules/all-modules.nix { 103 | inherit lib pkgs; 104 | check = false; 105 | modules = builtins.attrValues self.projectModules; 106 | } 107 | ++ [scrubbedPkgsModule]; 108 | variablelistId = "project-manager-options"; 109 | }; 110 | 111 | release-config = import ../release.nix; 112 | revision = "release-${release-config.release}"; 113 | # Generate the `man project-configuration.nix` package 114 | project-configuration-manual = 115 | pkgs.runCommand "project-configuration-reference-manpage" { 116 | nativeBuildInputs = [pkgs.buildPackages.installShellFiles pkgs.nixos-render-docs]; 117 | allowedReferences = ["out"]; 118 | } '' 119 | # Generate manpages. 120 | mkdir -p $out/share/man/man5 121 | mkdir -p $out/share/man/man1 122 | nixos-render-docs -j $NIX_BUILD_CORES options manpage \ 123 | --revision ${revision} \ 124 | --header ${./project-configuration-nix-header.5} \ 125 | --footer ${./project-configuration-nix-footer.5} \ 126 | ${pmOptionsDocs.optionsJSON}/share/doc/nixos/options.json \ 127 | $out/share/man/man5/project-configuration.nix.5 128 | cp ${./project-manager.1} $out/share/man/man1/project-manager.1 129 | ''; 130 | # Generate the HTML manual pages 131 | project-manager-manual = pkgs.callPackage ./project-manager-manual.nix { 132 | project-manager-options = { 133 | project-manager = pmOptionsDocs.optionsJSON; 134 | }; 135 | inherit revision; 136 | }; 137 | html = project-manager-manual; 138 | htmlOpenTool = pkgs.callPackage ./html-open-tool.nix {} {inherit html;}; 139 | in { 140 | options = { 141 | # TODO: Use `pmOptionsDocs.optionsJSON` directly once upstream 142 | # `nixosOptionsDoc` is more customizable. 143 | json = 144 | pkgs.runCommand "options.json" { 145 | meta.description = "List of Project Manager options in JSON format"; 146 | } '' 147 | mkdir -p $out/{share/doc,nix-support} 148 | cp -a ${pmOptionsDocs.optionsJSON}/share/doc/nixos $out/share/doc/project-manager 149 | substitute \ 150 | ${pmOptionsDocs.optionsJSON}/nix-support/hydra-build-products \ 151 | $out/nix-support/hydra-build-products \ 152 | --replace \ 153 | '${pmOptionsDocs.optionsJSON}/share/doc/nixos' \ 154 | "$out/share/doc/project-manager" 155 | ''; 156 | }; 157 | 158 | manPages = project-configuration-manual; 159 | 160 | manual = {inherit html htmlOpenTool;}; 161 | 162 | # Unstable, mainly for CI. 163 | jsonModuleMaintainers = pkgs.writeText "pm-module-maintainers.json" (let 164 | result = lib.evalModules { 165 | modules = 166 | import ../modules/all-modules.nix { 167 | inherit lib pkgs; 168 | check = false; 169 | modules = builtins.attrValues self.projectModules; 170 | } 171 | ++ [scrubbedPkgsModule]; 172 | class = "projectManager"; 173 | }; 174 | in 175 | builtins.toJSON result.config.meta.maintainers); 176 | } 177 | --------------------------------------------------------------------------------