├── default.nix ├── repos ├── elpa │ ├── test.nix │ └── update ├── nongnu │ ├── test.nix │ └── update ├── emacs │ ├── emacs-unstable.json │ ├── emacs-lsp.json │ ├── emacs-master.json │ ├── emacs-feature_igc.json │ ├── commercial-emacs-commercial-emacs.json │ ├── test.nix │ ├── update-unstable.py │ └── update ├── melpa │ ├── test.nix │ └── update └── fromElisp │ ├── update │ └── default.nix ├── .github ├── dependabot.yml └── workflows │ ├── pulls.yml │ └── ci.yml ├── overlays ├── default.nix ├── package.nix └── emacs.nix ├── update ├── packreq.nix ├── flake.lock ├── flake.nix ├── elisp.nix ├── parse.nix └── README.org /default.nix: -------------------------------------------------------------------------------- 1 | self: super: 2 | import ./overlays self super 3 | -------------------------------------------------------------------------------- /repos/elpa/test.nix: -------------------------------------------------------------------------------- 1 | let 2 | 3 | pkgs = import { 4 | overlays = [ 5 | (import ../../default.nix) 6 | ]; 7 | }; 8 | 9 | in pkgs.emacs.pkgs 10 | -------------------------------------------------------------------------------- /repos/nongnu/test.nix: -------------------------------------------------------------------------------- 1 | let 2 | 3 | pkgs = import { 4 | overlays = [ 5 | (import ../../default.nix) 6 | ]; 7 | }; 8 | 9 | in pkgs.emacs.pkgs 10 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | target-branch: "master" 6 | schedule: 7 | interval: "weekly" 8 | -------------------------------------------------------------------------------- /repos/emacs/emacs-unstable.json: -------------------------------------------------------------------------------- 1 | {"type": "savannah", "url": "https://https.git.savannah.gnu.org/git/emacs.git", "rev": "emacs-30.2", "sha256": "083m092ad4djy5r5g3jx3wchdbm5wzpcapq2ky97m5cxgbfdpdyw", "version": "30.2"} 2 | -------------------------------------------------------------------------------- /repos/emacs/emacs-lsp.json: -------------------------------------------------------------------------------- 1 | {"type": "github", "owner": "emacs-lsp", "repo": "emacs", "rev": "f28010891665f1a2ebc9038a203e3ec870ce792c", "sha256": "01s6c29d2dggsk6088yibk3r9ifiz1xl9jbir2brcsllrba8cx4s", "version": "20221208.0"} 2 | -------------------------------------------------------------------------------- /repos/melpa/test.nix: -------------------------------------------------------------------------------- 1 | let 2 | 3 | pkgs = import { 4 | overlays = [ 5 | (import ../../default.nix) 6 | ]; 7 | }; 8 | 9 | in { 10 | inherit (pkgs.emacs.pkgs) melpaStablePackages melpaPackages; 11 | } 12 | -------------------------------------------------------------------------------- /repos/emacs/emacs-master.json: -------------------------------------------------------------------------------- 1 | {"type": "savannah", "url": "https://https.git.savannah.gnu.org/git/emacs.git", "rev": "54ae1944e95c77be6492d69792413e507c2dfdb0", "sha256": "16hmd0ckk8spw6zy8clpyl9z1x1fahqhmd3g0wjvk3fv8x6dnzn1", "version": "20251224.0"} 2 | -------------------------------------------------------------------------------- /repos/emacs/emacs-feature_igc.json: -------------------------------------------------------------------------------- 1 | {"type": "savannah", "url": "https://https.git.savannah.gnu.org/git/emacs.git", "rev": "03b258fe443ef1cbdeea516d8cf85077e5d6a3dc", "sha256": "1n2i9bv552fb3qhy8gi4ci29fgrdn6w9dc1aqi6ndxls4pbpp2n8", "version": "20251206.0"} 2 | -------------------------------------------------------------------------------- /overlays/default.nix: -------------------------------------------------------------------------------- 1 | self: super: 2 | let 3 | overlays = [ 4 | # package overlay must be applied before emacs overlay 5 | (import ./package.nix) 6 | (import ./emacs.nix) 7 | ]; 8 | in 9 | super.lib.composeManyExtensions overlays self super 10 | -------------------------------------------------------------------------------- /repos/emacs/commercial-emacs-commercial-emacs.json: -------------------------------------------------------------------------------- 1 | {"type": "github", "owner": "commercial-emacs", "repo": "commercial-emacs", "rev": "7cb6fb788c34570bceb9c568dd75f6443c2ef3c8", "sha256": "16lwi76pnla7v66j5fwirkc8xzszqn5jh1m41lpiy5s0zkhx235i", "version": "20251217.0"} 2 | -------------------------------------------------------------------------------- /update: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env nix-shell 2 | #! nix-shell -i bash -p bash git 3 | 4 | set -euo pipefail 5 | 6 | repo="$1" 7 | 8 | "repos/$repo/update" 9 | 10 | if git diff --exit-code "repos/$repo" > /dev/null; then 11 | exit 0 12 | fi 13 | 14 | git commit -m "Updated $repo" -- "repos/$repo" 15 | -------------------------------------------------------------------------------- /repos/emacs/test.nix: -------------------------------------------------------------------------------- 1 | { pkgs ? import { overlays = [ (import ../../default.nix) ]; } }: 2 | 3 | let 4 | mkTestBuild = package: let 5 | emacsPackages = pkgs.emacsPackagesFor package; 6 | emacsWithPackages = emacsPackages.emacsWithPackages; 7 | in emacsWithPackages(epkgs: [ ]); 8 | 9 | in { 10 | emacsUnstable = mkTestBuild pkgs.emacsUnstable; 11 | emacsGit = mkTestBuild pkgs.emacsGit; 12 | emacsPgtk = mkTestBuild pkgs.emacsPgtk; 13 | emacsIgc = mkTestBuild pkgs.emacs-igc; 14 | } 15 | -------------------------------------------------------------------------------- /.github/workflows/pulls.yml: -------------------------------------------------------------------------------- 1 | name: Checks 2 | 3 | on: 4 | pull_request: 5 | push: 6 | 7 | jobs: 8 | flake-show: 9 | runs-on: ubuntu-latest 10 | outputs: 11 | matrix: ${{ steps.set-matrix.outputs.matrix }} 12 | steps: 13 | - uses: actions/checkout@v6.0.1 14 | - uses: cachix/install-nix-action@v31 15 | with: 16 | nix_path: nixpkgs=channel:nixos-unstable 17 | - id: set-matrix 18 | name: Evaluate flake 19 | run: nix flake show --all-systems 20 | -------------------------------------------------------------------------------- /repos/fromElisp/update: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env nix-shell 2 | #! nix-shell -i bash -p curl 3 | set -euxo pipefail 4 | 5 | SCRIPTPATH="$( cd "$(dirname "$0")" ; pwd -P )" 6 | cd $SCRIPTPATH 7 | 8 | curl -Of https://raw.githubusercontent.com/talyz/fromElisp/master/default.nix 9 | 10 | cat < default.nix 11 | # WARNING: This file was automatically imported from 12 | # https://github.com/talyz/fromElisp. Don't make any changes to it 13 | # locally - they will be discarded on update! 14 | 15 | $(cat default.nix) 16 | EOF 17 | -------------------------------------------------------------------------------- /repos/nongnu/update: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env nix-shell 2 | #! nix-shell -i bash -p bash 3 | set -euxo pipefail 4 | 5 | SCRIPTPATH="$( cd "$(dirname "$0")" ; pwd -P )" 6 | cd $SCRIPTPATH 7 | 8 | eval $(nix-instantiate --eval --expr 'let nixpath = builtins.toString (import {}).path; in "${nixpath}/pkgs/applications/editors/emacs/elisp-packages/update-nongnu"') 9 | env NIXPKGS_ALLOW_BROKEN=1 nix-instantiate --show-trace ./test.nix -A nongnuPackages 10 | 11 | eval $(nix-instantiate --eval --expr 'let nixpath = builtins.toString (import {}).path; in "${nixpath}/pkgs/applications/editors/emacs/elisp-packages/update-nongnu-devel"') 12 | env NIXPKGS_ALLOW_BROKEN=1 nix-instantiate --show-trace ./test.nix -A nongnuDevelPackages 13 | -------------------------------------------------------------------------------- /repos/elpa/update: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env nix-shell 2 | #! nix-shell -i bash -p bash 3 | set -euxo pipefail 4 | 5 | SCRIPTPATH="$( cd "$(dirname "$0")" ; pwd -P )" 6 | cd $SCRIPTPATH 7 | 8 | rm elpa-generated.nix 9 | 10 | eval $(nix-instantiate --eval --expr 'let nixpath = builtins.toString (import {}).path; in "${nixpath}/pkgs/applications/editors/emacs/elisp-packages/update-elpa"') 11 | env NIXPKGS_ALLOW_BROKEN=1 nix-instantiate --show-trace ./test.nix -A elpaPackages 12 | 13 | rm elpa-devel-generated.nix 14 | 15 | eval $(nix-instantiate --eval --expr 'let nixpath = builtins.toString (import {}).path; in "${nixpath}/pkgs/applications/editors/emacs/elisp-packages/update-elpa-devel"') 16 | env NIXPKGS_ALLOW_BROKEN=1 nix-instantiate --show-trace ./test.nix -A elpaDevelPackages 17 | -------------------------------------------------------------------------------- /repos/melpa/update: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env nix-shell 2 | #! nix-shell -i bash -p bash jq 3 | set -euxo pipefail 4 | 5 | SCRIPTPATH="$( cd "$(dirname "$0")" ; pwd -P )" 6 | cd $SCRIPTPATH 7 | 8 | # Hack updater env not to use broken Nix 9 | elisp_packages=$(nix-instantiate --eval --expr 'let nixpath = builtins.toString (import {}).path; in "${nixpath}/pkgs/applications/editors/emacs/elisp-packages"' | jq -r) 10 | update_script="$elisp_packages/update-melpa" 11 | update_env="$elisp_packages/updater-emacs.nix" 12 | sed s/'pkgs = .*'/'pkgs = import { };'/ "$update_env" | grep -vP ' *pkgs\.nix$' > shell.nix 13 | 14 | nix-shell shell.nix --run "bash $update_script" 15 | 16 | env NIXPKGS_ALLOW_BROKEN=1 nix-instantiate --show-trace ./test.nix -A melpaStablePackages 17 | env NIXPKGS_ALLOW_BROKEN=1 nix-instantiate --show-trace ./test.nix -A melpaPackages 18 | -------------------------------------------------------------------------------- /packreq.nix: -------------------------------------------------------------------------------- 1 | /* 2 | Parse an emacs package file to derive packages from 3 | Package-Requires declarations. 4 | */ 5 | 6 | { pkgs }: 7 | let 8 | parse = pkgs.callPackage ./parse.nix { }; 9 | in 10 | { packageElisp 11 | , extraEmacsPackages ? epkgs: [ ] 12 | , package ? pkgs.emacs 13 | , override ? (self: super: { }) 14 | }: 15 | let 16 | packages = parse.parsePackagesFromPackageRequires packageElisp; 17 | emacsPackages = (pkgs.emacsPackagesFor package).overrideScope (self: super: 18 | # for backward compatibility: override was a function with one parameter 19 | if builtins.isFunction (override super) 20 | then override self super 21 | else override super 22 | ); 23 | emacsWithPackages = emacsPackages.emacsWithPackages; 24 | in 25 | emacsWithPackages (epkgs: 26 | let 27 | usePkgs = builtins.map (name: epkgs.${name}) packages; 28 | extraPkgs = extraEmacsPackages epkgs; 29 | in 30 | [ epkgs.use-package ] ++ usePkgs ++ extraPkgs) 31 | -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "nixpkgs": { 4 | "locked": { 5 | "lastModified": 1766309749, 6 | "narHash": "sha256-3xY8CZ4rSnQ0NqGhMKAy5vgC+2IVK0NoVEzDoOh4DA4=", 7 | "owner": "NixOS", 8 | "repo": "nixpkgs", 9 | "rev": "a6531044f6d0bef691ea18d4d4ce44d0daa6e816", 10 | "type": "github" 11 | }, 12 | "original": { 13 | "owner": "NixOS", 14 | "ref": "nixos-unstable", 15 | "repo": "nixpkgs", 16 | "type": "github" 17 | } 18 | }, 19 | "nixpkgs-stable": { 20 | "locked": { 21 | "lastModified": 1766399428, 22 | "narHash": "sha256-vS6LSOMDOB3s+L6tqw9IGujxnmUAZQnEG+Vi640LayI=", 23 | "owner": "NixOS", 24 | "repo": "nixpkgs", 25 | "rev": "a6c3a6141ec1b367c58ead3f7f846c772a25f4e5", 26 | "type": "github" 27 | }, 28 | "original": { 29 | "owner": "NixOS", 30 | "ref": "nixos-25.05", 31 | "repo": "nixpkgs", 32 | "type": "github" 33 | } 34 | }, 35 | "root": { 36 | "inputs": { 37 | "nixpkgs": "nixpkgs", 38 | "nixpkgs-stable": "nixpkgs-stable" 39 | } 40 | } 41 | }, 42 | "root": "root", 43 | "version": 7 44 | } 45 | -------------------------------------------------------------------------------- /overlays/package.nix: -------------------------------------------------------------------------------- 1 | self: super: 2 | { 3 | emacsPackagesFor = emacs: ( 4 | (super.emacsPackagesFor emacs).overrideScope ( 5 | eself: esuper: 6 | let 7 | melpaStablePackages = esuper.melpaStablePackages.override { 8 | archiveJson = ../repos/melpa/recipes-archive-melpa.json; 9 | }; 10 | 11 | melpaPackages = esuper.melpaPackages.override { 12 | archiveJson = ../repos/melpa/recipes-archive-melpa.json; 13 | }; 14 | 15 | elpaDevelPackages = esuper.elpaDevelPackages.override { 16 | generated = ../repos/elpa/elpa-devel-generated.nix; 17 | }; 18 | 19 | elpaPackages = (esuper.elpaPackages.override { 20 | generated = ../repos/elpa/elpa-generated.nix; 21 | }) // { 22 | # Tramp 2.8.0.4 has a broken tarball 23 | tramp = 24 | if esuper.elpaPackages.tramp.version != "2.8.0.4" then esuper.elpaPackages.tramp 25 | else esuper.elpaPackages.tramp.overrideAttrs { 26 | version = "2.8.0.3"; 27 | src = self.fetchurl { 28 | name = "tramp-2.8.0.3.tar"; 29 | url = "https://elpa.gnu.org/packages/tramp-2.8.0.3.tar.lz"; 30 | downloadToTemp = true; 31 | postFetch = '' 32 | cp $downloadedFile tramp-2.8.0.3.tar.lz 33 | ${self.lib.getExe self.lzip} -d tramp-2.8.0.3.tar.lz 34 | mv tramp-2.8.0.3.tar $out 35 | ''; 36 | hash = "sha256-o+heQw47btZhhM+5GtvzUZlqcNaoW3966fZyj8m6X+M="; 37 | }; 38 | }; 39 | }; 40 | 41 | nongnuDevelPackages = esuper.nongnuDevelPackages.override { 42 | generated = ../repos/nongnu/nongnu-devel-generated.nix; 43 | }; 44 | 45 | nongnuPackages = esuper.nongnuPackages.override { 46 | generated = ../repos/nongnu/nongnu-generated.nix; 47 | }; 48 | 49 | in 50 | esuper.override { 51 | inherit melpaStablePackages melpaPackages elpaDevelPackages elpaPackages 52 | nongnuDevelPackages nongnuPackages; 53 | } 54 | 55 | ) 56 | ); 57 | 58 | } 59 | -------------------------------------------------------------------------------- /repos/emacs/update-unstable.py: -------------------------------------------------------------------------------- 1 | from packaging.version import Version, InvalidVersion 2 | import subprocess 3 | import json 4 | 5 | # (Ab)use Python's PEP-440 version parsing for sorting Emacs versions. 6 | # Emacs versions aren't exactly PEP-440, but `git ls-remote`'s sort function does not support -rc 7 | # suffixes and neither does GNU sort. 8 | # Meaning that older -rc releases may be prefered to a later stable release. 9 | 10 | TAG_PREAMBLE = "refs/tags/emacs-" 11 | 12 | 13 | def main(): 14 | proc = subprocess.run( 15 | [ 16 | "git", 17 | "ls-remote", 18 | "--tags", 19 | "--refs", 20 | "https://https.git.savannah.gnu.org/git/emacs.git", 21 | "emacs-[1-9]*", 22 | ], 23 | stdout=subprocess.PIPE, 24 | check=True, 25 | ) 26 | 27 | tags: list[str] = [] 28 | for line in proc.stdout.decode().splitlines(): 29 | _commit, ref = line.split("\t") 30 | if not ref.startswith(TAG_PREAMBLE): 31 | continue 32 | 33 | tag = ref[len(TAG_PREAMBLE) :] 34 | 35 | # Skip unparseable versions 36 | try: 37 | Version(tag) 38 | except InvalidVersion: 39 | pass 40 | else: 41 | tags.append(tag) 42 | 43 | latest_version = sorted(tags, key=lambda tag: Version(tag))[-1] 44 | latest_tag = f"emacs-{latest_version}" 45 | 46 | proc = subprocess.run( 47 | [ 48 | "nix-prefetch-git", 49 | "--rev", 50 | f"refs/tags/{latest_tag}", 51 | "https://https.git.savannah.gnu.org/git/emacs.git", 52 | ], 53 | stdout=subprocess.PIPE, 54 | check=True, 55 | ) 56 | digest = json.loads(proc.stdout.decode().strip()) 57 | 58 | with open("./emacs-unstable.json", "w") as fp: 59 | json.dump( 60 | { 61 | "type": "savannah", 62 | "url": "https://https.git.savannah.gnu.org/git/emacs.git", 63 | "rev": latest_tag, 64 | "sha256": digest['sha256'], 65 | "version": latest_version, 66 | }, 67 | fp, 68 | ) 69 | fp.write("\n") 70 | 71 | 72 | if __name__ == "__main__": 73 | main() 74 | -------------------------------------------------------------------------------- /repos/emacs/update: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env nix-shell 2 | #! nix-shell -i bash -p curl xmlstarlet nix coreutils 'python3.withPackages(ps: [ ps.packaging ])' nix-prefetch-git jq 3 | set -euxo pipefail 4 | 5 | SCRIPTPATH="$( cd "$(dirname "$0")" ; pwd -P )" 6 | cd $SCRIPTPATH 7 | 8 | function update_savannah_branch() { 9 | branch=$1 10 | echo emacs $branch 11 | 12 | url="https://https.git.savannah.gnu.org/git/emacs.git" 13 | 14 | # extract the latest commit sha 15 | commit_sha=$(git ls-remote --branches "$url" refs/heads/$branch | cut -f 1) 16 | 17 | source_info=$(nix-prefetch-git --rev $commit_sha "$url") 18 | 19 | digest=$(echo $source_info | jq -r .sha256) 20 | # build a version number based on date: YYYYMMDD.0 21 | version_number=$(echo $source_info | jq -r .date | cut -d 'T' -f 1 | sed 's/-//g').0 22 | output_branch=$(echo $branch | sed s/"\/"/"_"/) 23 | echo "{\"type\": \"savannah\", \"url\": \"${url}\", \"rev\": \"${commit_sha}\", \"sha256\": \"${digest}\", \"version\": \"${version_number}\"}" > emacs-$output_branch.json 24 | } 25 | 26 | function update_github_repo() { 27 | owner=$1 28 | repo=$2 29 | branch=$3 30 | output_name=$4 31 | echo $repo $branch 32 | 33 | # Get relevant data (commit id and timestamp) for the latest commit 34 | commit_data=$(curl "https://github.com/$owner/$repo/commits/$branch.atom" | xmlstarlet sel -N atom="http://www.w3.org/2005/Atom" -t -m /atom:feed/atom:entry -v "concat(atom:id,'/',atom:updated)" -n | head -n 1) 35 | 36 | # Extract commit sha and build a version number based on date: YYYYMMDD.0 37 | commit_sha=$(echo $commit_data | cut -d '/' -f 2) 38 | version_number=$(echo $commit_data | cut -d '/' -f 3 | cut -d 'T' -f 1 | sed 's/-//g').0 39 | 40 | digest=$(nix-prefetch-url --unpack "https://github.com/$owner/$repo/archive/${commit_sha}.tar.gz") 41 | echo "{\"type\": \"github\", \"owner\": \"${owner}\", \"repo\": \"${repo}\", \"rev\": \"${commit_sha}\", \"sha256\": \"${digest}\", \"version\": \"${version_number}\"}" > $repo-$output_name.json 42 | } 43 | 44 | function update_unstable() { 45 | echo emacs unstable 46 | python3 ./update-unstable.py 47 | } 48 | 49 | update_savannah_branch master 50 | update_savannah_branch feature/igc 51 | update_unstable 52 | update_github_repo emacs-lsp emacs json-rpc lsp 53 | update_github_repo commercial-emacs commercial-emacs master commercial-emacs 54 | 55 | nix-build --no-out-link --show-trace ./test.nix 56 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Update emacs-overlay 2 | 3 | on: 4 | schedule: 5 | - cron: '0 */8 * * *' 6 | workflow_dispatch: 7 | 8 | jobs: 9 | flake-inputs: 10 | name: Update flake inputs 11 | runs-on: ubuntu-latest 12 | if: github.repository_owner == 'nix-community' 13 | steps: 14 | - name: Checkout repository 15 | uses: actions/checkout@v6.0.1 16 | 17 | - name: Install Nix 18 | uses: cachix/install-nix-action@v31 19 | with: 20 | nix_path: nixpkgs=channel:nixos-unstable 21 | 22 | - name: Add nix-community cache 23 | uses: cachix/cachix-action@v16 24 | with: 25 | name: nix-community 26 | authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}' 27 | 28 | - name: Configure Git 29 | run: | 30 | git config user.name github-actions 31 | git config user.email github-actions@github.com 32 | 33 | - name: Update flake inputs 34 | run: | 35 | nix flake update 36 | git commit -m "Updated flake inputs" flake.lock || true 37 | 38 | - name: Push commit with updated inputs 39 | run: | 40 | git pull --rebase --autostash 41 | git push 42 | 43 | refresh-overlay: 44 | name: Refresh inputs 45 | needs: flake-inputs 46 | runs-on: ubuntu-latest 47 | strategy: 48 | matrix: 49 | repo: ["elpa", "emacs", "melpa", "fromElisp", "nongnu"] 50 | if: github.repository_owner == 'nix-community' 51 | steps: 52 | - name: Checkout repository 53 | uses: actions/checkout@v6.0.1 54 | 55 | - name: Install Nix 56 | uses: cachix/install-nix-action@v31 57 | with: 58 | nix_path: nixpkgs=channel:nixos-unstable 59 | 60 | - name: Add nix-community cache 61 | uses: cachix/cachix-action@v16 62 | with: 63 | name: nix-community 64 | authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}' 65 | 66 | - name: Configure Git 67 | run: | 68 | git config user.name github-actions 69 | git config user.email github-actions@github.com 70 | 71 | - name: Update inputs 72 | run: | 73 | export GIT_PAGER= 74 | ./update ${{ matrix.repo }} 75 | 76 | - name: Push commit with updated inputs 77 | run: | 78 | # work around race of git between elpa and nongnu 79 | # Between elpa runs `git pull` and `git push`, nongnu may run `git 80 | # push`. Let elpa sleep for 20 seconds and hopefully nongnu has 81 | # finished `git push`. 82 | [[ ${{ matrix.repo }} == "elpa" ]] && sleep 20 83 | git pull --rebase --autostash 84 | git push 85 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | description = "Bleeding edge Emacs overlay"; 3 | 4 | nixConfig = { 5 | extra-substituters = [ "https://nix-community.cachix.org" ]; 6 | extra-trusted-public-keys = [ "nix-community.cachix.org-1:mB9FSh9qf2dCimDSUo8Zy7bkq5CX+/rkCWyvRCYg3Fs=" ]; 7 | }; 8 | 9 | inputs = { 10 | nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; 11 | nixpkgs-stable.url = "github:NixOS/nixpkgs/nixos-25.05"; 12 | }; 13 | 14 | outputs = 15 | { self 16 | , nixpkgs 17 | , nixpkgs-stable 18 | }: 19 | let 20 | inherit (nixpkgs) lib; 21 | forAllSystems = lib.genAttrs lib.systems.flakeExposed; 22 | 23 | importPkgs = path: attrs: import path (attrs // { 24 | config.allowAliases = false; 25 | overlays = [ self.overlays.default ]; 26 | }); 27 | 28 | packages' = forAllSystems (system: ( 29 | let 30 | pkgs = importPkgs nixpkgs { inherit system; }; 31 | inherit (pkgs) lib; 32 | 33 | overlayAttributes = lib.pipe (import ./. pkgs pkgs) [ 34 | builtins.attrNames 35 | (lib.partition (n: lib.isDerivation pkgs.${n})) 36 | ]; 37 | attributesToAttrset = attributes: lib.pipe attributes [ 38 | (map (n: lib.nameValuePair n pkgs.${n})) 39 | lib.listToAttrs 40 | ]; 41 | 42 | in 43 | { 44 | lib = attributesToAttrset overlayAttributes.wrong; 45 | packages = attributesToAttrset overlayAttributes.right; 46 | } 47 | )); 48 | 49 | in 50 | { 51 | # self: super: must be named final: prev: for `nix flake check` to be happy 52 | overlays = { 53 | default = final: prev: import ./overlays final prev; 54 | emacs = final: prev: import ./overlays/emacs.nix final prev; 55 | package = final: prev: import ./overlays/package.nix final prev; 56 | }; 57 | # for backward compatibility, is safe to delete, not referenced anywhere 58 | overlay = self.overlays.default; 59 | 60 | hydraJobs = 61 | lib.genAttrs [ "x86_64-linux" "aarch64-linux" ] (system: 62 | let 63 | mkHydraJobs = pkgs: 64 | let 65 | inherit (pkgs) lib; 66 | 67 | filterNonDrvAttrs = s: lib.mapAttrs (_: v: if (lib.isDerivation v) then v else filterNonDrvAttrs v) (lib.filterAttrs (_: v: lib.isDerivation v || (builtins.typeOf v == "set" && ! builtins.hasAttr "__functor" v)) s); 68 | 69 | mkEmacsSet = emacs: filterNonDrvAttrs (pkgs.recurseIntoAttrs (pkgs.emacsPackagesFor emacs)); 70 | 71 | in 72 | { 73 | emacsen = { 74 | inherit (pkgs) emacs-unstable emacs-unstable-nox; 75 | inherit (pkgs) emacs-unstable-pgtk; 76 | inherit (pkgs) emacs-git emacs-git-nox; 77 | inherit (pkgs) emacs-git-pgtk; 78 | inherit (pkgs) emacs-igc emacs-igc-pgtk; 79 | inherit (pkgs) emacs-lsp; 80 | inherit (pkgs) commercial-emacs; 81 | }; 82 | }; 83 | 84 | in 85 | { 86 | "stable" = mkHydraJobs (importPkgs nixpkgs-stable { inherit system; }); 87 | "unstable" = mkHydraJobs (importPkgs nixpkgs { inherit system; }); 88 | }); 89 | 90 | packages = forAllSystems (system: packages'.${system}.packages); 91 | lib = forAllSystems (system: packages'.${system}.lib); 92 | }; 93 | } 94 | -------------------------------------------------------------------------------- /elisp.nix: -------------------------------------------------------------------------------- 1 | /* 2 | Parse an emacs lisp configuration file to derive packages from 3 | use-package declarations. 4 | */ 5 | 6 | { pkgs }: 7 | let 8 | parse = pkgs.callPackage ./parse.nix { }; 9 | inherit (pkgs) lib; 10 | 11 | 12 | 13 | in 14 | { config 15 | # bool to use the value of config or a derivation whose name is default.el 16 | , defaultInitFile ? false 17 | # emulate `use-package-always-ensure` behavior (defaulting to false) 18 | , alwaysEnsure ? false 19 | # emulate `#+PROPERTY: header-args:emacs-lisp :tangle yes` 20 | , alwaysTangle ? false 21 | , extraEmacsPackages ? epkgs: [ ] 22 | , package ? pkgs.emacs 23 | , override ? (self: super: { }) 24 | }: 25 | let 26 | isOrgModeFile = 27 | let 28 | ext = lib.last (builtins.split "\\." (builtins.toString config)); 29 | type = builtins.typeOf config; 30 | in 31 | (type == "path" || lib.hasPrefix "/" config) && ext == "org"; 32 | 33 | configText = 34 | let 35 | type = builtins.typeOf config; 36 | in # configText can be sourced from either: 37 | # - A string with context { config = "${hello}/config.el"; } 38 | if type == "string" && builtins.hasContext config && lib.hasPrefix builtins.storeDir config then builtins.readFile config 39 | # - A config literal { config = "(use-package foo)"; } 40 | else if type == "string" then config 41 | # - A config path { config = ./config.el; } 42 | else if type == "path" then builtins.readFile config 43 | # - A derivation { config = pkgs.writeText "config.el" "(use-package foo)"; } 44 | else if lib.isDerivation config then builtins.readFile "${config}" 45 | else throw "Unsupported type for config: \"${type}\""; 46 | 47 | packages = parse.parsePackagesFromUsePackage { 48 | inherit configText isOrgModeFile alwaysTangle alwaysEnsure; 49 | }; 50 | emacsPackages = (pkgs.emacsPackagesFor package).overrideScope (self: super: 51 | # for backward compatibility: override was a function with one parameter 52 | if builtins.isFunction (override super) 53 | then override self super 54 | else override super 55 | ); 56 | emacsWithPackages = emacsPackages.emacsWithPackages; 57 | mkPackageError = name: 58 | let 59 | errorFun = if (alwaysEnsure != null && alwaysEnsure) then builtins.trace else throw; 60 | in 61 | errorFun "Emacs package ${name}, declared wanted with use-package, not found." null; 62 | in 63 | emacsWithPackages (epkgs: 64 | let 65 | usePkgs = map (name: epkgs.${name} or (mkPackageError name)) packages; 66 | extraPkgs = extraEmacsPackages epkgs; 67 | defaultInitFilePkg = 68 | if !((builtins.isBool defaultInitFile) || (lib.isDerivation defaultInitFile)) 69 | then throw "defaultInitFile must be bool or derivation" 70 | else 71 | if defaultInitFile == false 72 | then null 73 | else 74 | let 75 | # name of the default init file must be default.el according to elisp manual 76 | defaultInitFileName = "default.el"; 77 | configFile = pkgs.writeText defaultInitFileName configText; 78 | orgModeConfigFile = pkgs.runCommand defaultInitFileName { 79 | nativeBuildInputs = [ package ]; 80 | } '' 81 | cp ${configFile} config.org 82 | emacs -Q --batch ./config.org -f org-babel-tangle 83 | mv config.el $out 84 | ''; 85 | in 86 | epkgs.trivialBuild { 87 | pname = "default"; 88 | src = 89 | if defaultInitFile == true 90 | then 91 | if isOrgModeFile 92 | then orgModeConfigFile 93 | else configFile 94 | else 95 | if defaultInitFile.name == defaultInitFileName 96 | then defaultInitFile 97 | else throw "name of defaultInitFile must be ${defaultInitFileName}"; 98 | version = "0.1.0"; 99 | packageRequires = usePkgs ++ extraPkgs; 100 | }; 101 | in 102 | usePkgs ++ extraPkgs ++ [ defaultInitFilePkg ]) 103 | -------------------------------------------------------------------------------- /parse.nix: -------------------------------------------------------------------------------- 1 | { pkgs, lib }: 2 | 3 | let 4 | inherit (import ./repos/fromElisp { inherit pkgs; }) fromElisp fromOrgModeBabelElisp'; 5 | 6 | isStrEmpty = s: (builtins.replaceStrings [ " " ] [ "" ] s) == ""; 7 | 8 | splitString = _sep: _s: builtins.filter 9 | (x: builtins.typeOf x == "string") 10 | (builtins.split _sep _s); 11 | 12 | # Parse (all) Package-Requires packageElisp headers found in the input string 13 | # `packageElisp` into a list of package name strings. 14 | # 15 | # Example inputs: 16 | # 17 | # ;; Package-Requires: () 18 | # => [ ] 19 | # ;; Package-Requires: ((dash "2.12.1") (pkg-info "0.4") (let-alist "1.0.4") (seq "1.11") (emacs "24.3")) 20 | # => [ "dash" "pkg-info" "let-alist" "seq" "emacs" ] 21 | # ;; Package-Requires: (dash (pkg-info "0.4")) 22 | # => [ "dash" "pkg-info" ] 23 | # ;; Package-Requires: ((dash) (pkg-info "0.4")) 24 | # => [ "dash" "pkg-info" ] 25 | parsePackagesFromPackageRequires = packageElisp: 26 | let 27 | lines = splitString "\r?\n" packageElisp; 28 | requires = 29 | lib.concatMapStrings 30 | (line: 31 | let match = builtins.match ";;;* *[pP]ackage-[rR]equires *: *\\((.*)\\) *" line; 32 | in if match == null then "" else builtins.head match) 33 | lines; 34 | parseReqList = s: 35 | let matchAndRest = builtins.match " *\\(? *([^ \"\\)]+)( +\"[^\"]+\" *\\)| *\\))?(.*)" s; 36 | in 37 | if isStrEmpty s then 38 | [ ] 39 | else 40 | if matchAndRest == null then 41 | throw "Failed to parse package requirements list: ${s}" 42 | else 43 | [ (builtins.head matchAndRest) ] ++ (parseReqList (builtins.elemAt matchAndRest 2)); 44 | in 45 | parseReqList requires; 46 | 47 | # Get a list of packages declared wanted with `use-package` in the 48 | # input string `config`. The goal is to only list packages that 49 | # would be installed by `use-package` on evaluation; thus we look at 50 | # the `:ensure` and `:disabled` keyword values to attempt to figure 51 | # out which and whether the package should be installed. 52 | # 53 | # Example input: 54 | # 55 | # '' 56 | # (use-package org 57 | # :commands org-mode 58 | # :bind (("C-c a" . org-agenda) 59 | # :map org-mode-map 60 | # ([C-right] . org-demote-subtree) 61 | # ([C-left] . org-promote-subtree))) 62 | # 63 | # (use-package direnv 64 | # :ensure t 65 | # :config (direnv-mode)) 66 | # 67 | # (use-package paredit-mode 68 | # :ensure paredit 69 | # :hook (emacs-lisp-mode lisp-mode lisp-interaction-mode)) 70 | # '' 71 | # => [ "direnv" "paredit" ] 72 | parsePackagesFromUsePackage = { 73 | configText 74 | , alwaysEnsure ? false 75 | , isOrgModeFile ? false 76 | , alwaysTangle ? false 77 | }: 78 | let 79 | readFunction = 80 | if isOrgModeFile then 81 | fromOrgModeBabelElisp' { ":tangle" = if alwaysTangle then "yes" else "no"; } 82 | else 83 | fromElisp; 84 | 85 | find = item: list: 86 | if list == [] then [] else 87 | if builtins.head list == item then 88 | list 89 | else 90 | find item (builtins.tail list); 91 | 92 | getKeywordValue = keyword: list: 93 | let 94 | keywordList = find keyword list; 95 | in 96 | if keywordList != [] then 97 | let 98 | keywordValue = builtins.tail keywordList; 99 | in 100 | if keywordValue != [] then 101 | builtins.head keywordValue 102 | else 103 | true 104 | else 105 | null; 106 | 107 | isDisabled = item: 108 | let 109 | disabledValue = getKeywordValue ":disabled" item; 110 | in 111 | if disabledValue == [] then 112 | false 113 | else if builtins.isBool disabledValue then 114 | disabledValue 115 | else if builtins.isString disabledValue then 116 | true 117 | else 118 | false; 119 | 120 | getName = item: 121 | let 122 | ensureValue = getKeywordValue ":ensure" item; 123 | usePackageName = builtins.head (builtins.tail item); 124 | in 125 | if builtins.isString ensureValue then 126 | if lib.hasPrefix ":" ensureValue then 127 | usePackageName 128 | else 129 | ensureValue 130 | else if ensureValue == true || (ensureValue == null && alwaysEnsure) then 131 | usePackageName 132 | else 133 | []; 134 | 135 | recurse = item: 136 | if builtins.isList item && item != [] then 137 | let 138 | packageManager = builtins.head item; 139 | in 140 | if builtins.elem packageManager [ "use-package" "leaf" ] then 141 | if !(isDisabled item) then 142 | [ packageManager (getName item) ] ++ map recurse item 143 | else 144 | [] 145 | else 146 | map recurse item 147 | else 148 | []; 149 | in 150 | lib.flatten (map recurse (readFunction configText)); 151 | 152 | in 153 | { 154 | inherit parsePackagesFromPackageRequires; 155 | inherit parsePackagesFromUsePackage; 156 | } 157 | -------------------------------------------------------------------------------- /README.org: -------------------------------------------------------------------------------- 1 | * Emacs overlay for Nixpkgs 2 | ** Quickstart 3 | To get up and running quickly, add the following lines to your =/etc/nixos/configuration.nix=: 4 | 5 | #+BEGIN_SRC nix 6 | {config, pkgs, callPackage, ... }: 7 | { 8 | # ... 9 | 10 | services.emacs.package = pkgs.emacs-unstable; 11 | 12 | nixpkgs.overlays = [ 13 | (import (builtins.fetchTarball { 14 | url = "https://github.com/nix-community/emacs-overlay/archive/master.tar.gz"; 15 | })) 16 | ]; 17 | 18 | # ... 19 | } 20 | #+END_SRC 21 | 22 | This configuration will enable this overlay, and define your system-wide emacs package as the =emacs-unstable= attribute it provides. 23 | 24 | *NOTE:* Read the "Usage of the overlay" section below for further explanation of this configuration. This has the potential to break things, and will frequently trigger full source rebuilds of emacs. 25 | 26 | If you want to enable daemon/server mode, add the following line to the same configuration: 27 | 28 | #+BEGIN_SRC nix 29 | services.emacs.enable = true; 30 | #+END_SRC 31 | 32 | It is recommended you read Nixpkgs and NixOS documentation on package overlays and overrides to familiarize yourself with the concepts: 33 | 34 | - https://wiki.nixos.org/wiki/Overlays 35 | - https://nixos.org/nixpkgs/manual/#chap-overlays 36 | 37 | ** Contents of the overlay 38 | 39 | This overlay consists of two overlays: =emacs= and =package=. 40 | 41 | You can use both of them as a whole overlay or only one of them. 42 | 43 | *** =package= overlay 44 | 45 | **** Elpa 46 | Daily generations of Elpa. 47 | 48 | **** Melpa / Melpa stable 49 | Daily generations of Melpa & Melpa stable attribute sets. 50 | 51 | **** EXWM & needed dependencies 52 | This overlay provides fresh versions of EXWM and dependencies. This is 53 | updated daily. 54 | 55 | *** =emacs= overlay 56 | 57 | **** Emacs from Git and latest (including pre-releases) 58 | This overlay also provides two versions (latest from git) for Emacs. These 59 | are updated daily. 60 | 61 | These attributes are named =emacs-git= and =emacs-unstable=. 62 | =emacs-git= is built from the latest =master= branch and =emacs-unstable= is built from the latest tag. 63 | 64 | Emacs from git is not guaranteed stable and may break your setup at any 65 | time, if it breaks you get to keep both pieces. 66 | 67 | We also provide two attributes named =emacs-git-nox= and =emacs-unstable-nox= 68 | if you wish to have Emacs built without X dependencies. 69 | 70 | Additionally, the two attributes =emacs-git-pgtk= and =emacs-unstable-pgtk= enable 71 | the pure GTK (PGTK) feature, which is incompatible with X and supports Wayland 72 | natively. 73 | 74 | **** Extra library functionality 75 | This overlay comes with extra functions to generate an Emacs closure 76 | from various types of dependency declaration. (These are abstractions 77 | on top of =emacsWithPackages=.) 78 | 79 | For example, =emacsWithPackagesFromUsePackage= adds packages which are 80 | required in a user's config via =use-package= or =leaf=. 81 | 82 | #+BEGIN_SRC nix 83 | { pkgs, ... }: 84 | { 85 | environment.systemPackages = [ 86 | (pkgs.emacsWithPackagesFromUsePackage { 87 | # Your Emacs config file. Org mode babel files are also 88 | # supported. 89 | # NB: Config files cannot contain unicode characters, since 90 | # they're being parsed in nix, which lacks unicode 91 | # support. 92 | # config = ./emacs.org; 93 | config = ./emacs.el; 94 | 95 | # Whether to include your config as a default init file. 96 | # If being bool, the value of config is used. 97 | # Its value can also be a derivation like this if you want to do some 98 | # substitution: 99 | # defaultInitFile = pkgs.substituteAll { 100 | # name = "default.el"; 101 | # src = ./emacs.el; 102 | # inherit (config.xdg) configHome dataHome; 103 | # }; 104 | defaultInitFile = true; 105 | 106 | # Package is optional, defaults to pkgs.emacs 107 | package = pkgs.emacs-git; 108 | 109 | # By default emacsWithPackagesFromUsePackage will only pull in 110 | # packages with `:ensure`, `:ensure t` or `:ensure `. 111 | # Setting `alwaysEnsure` to `true` emulates `use-package-always-ensure` 112 | # and pulls in all use-package references not explicitly disabled via 113 | # `:ensure nil` or `:disabled`. 114 | # Note that this is NOT recommended unless you've actually set 115 | # `use-package-always-ensure` to `t` in your config. 116 | alwaysEnsure = true; 117 | 118 | # For Org mode babel files, by default only code blocks with 119 | # `:tangle yes` are considered. Setting `alwaysTangle` to `true` 120 | # will include all code blocks missing the `:tangle` argument, 121 | # defaulting it to `yes`. 122 | # Note that this is NOT recommended unless you have something like 123 | # `#+PROPERTY: header-args:emacs-lisp :tangle yes` in your config, 124 | # which defaults `:tangle` to `yes`. 125 | alwaysTangle = true; 126 | 127 | # Optionally provide extra packages not in the configuration file. 128 | # This can also include extra executables to be run by Emacs (linters, 129 | # language servers, formatters, etc) 130 | extraEmacsPackages = epkgs: [ 131 | epkgs.cask 132 | pkgs.shellcheck 133 | ]; 134 | 135 | # Optionally override derivations. 136 | override = final: prev: { 137 | weechat = prev.melpaPackages.weechat.overrideAttrs(old: { 138 | patches = [ ./weechat-el.patch ]; 139 | }); 140 | }; 141 | }) 142 | ]; 143 | } 144 | #+END_SRC 145 | 146 | Similarly, =emacsWithPackagesFromPackageRequires= adds packages which 147 | are declared in a =.el= package file's =Package-Requires= header, which 148 | can be handy for CI purposes: 149 | 150 | #+BEGIN_SRC nix 151 | ... 152 | let 153 | emacsForCI = pkgs.emacsWithPackagesFromPackageRequires { 154 | packageElisp = builtins.readFile ./flycheck.el; 155 | extraEmacsPackages = epkgs: [ 156 | epkgs.package-lint 157 | ]; 158 | }; 159 | pkgs.mkShell { 160 | buildInputs = [ emacsForCI ]; 161 | } 162 | #+END_SRC 163 | 164 | 165 | ** Usage of the overlay 166 | *** Latest master each rebuild 167 | One way, and probably the most convenient way to pull in this overlay is by 168 | just fetching the tarball of latest master on rebuild. 169 | 170 | This has side-effects if packages breaks or things like that you may want 171 | to be in control of which revision of the overlay you run. 172 | 173 | Adding the overlay this way will extend your Emacs packages set to contain 174 | the latest EXWM and dependencies from their respective master and make the 175 | package =emacs-git= available. These of course change quite rapidly and will 176 | cause compilation time. 177 | 178 | #+BEGIN_SRC nix 179 | { 180 | nixpkgs.overlays = [ 181 | (import (builtins.fetchTarball { 182 | url = "https://github.com/nix-community/emacs-overlay/archive/master.tar.gz"; 183 | })) 184 | ]; 185 | } 186 | #+END_SRC 187 | 188 | *** Binary cache 189 | You will want to use the [[https://nix-community.org/cache/][nix-community binary cache]]. Where the 190 | overlay's build artefacts are pushed. See [[https://app.cachix.org/cache/nix-community][here]] for installation 191 | instructions. 192 | 193 | *** Install directly from the overlay 194 | The repository is meant to be used as an overlay as is explained 195 | above. Still, for experimental purposes, you might want to install a 196 | package directly from the overlay. For example, you can install 197 | =emacs-git= from a clone of this repository with the following command: 198 | 199 | #+begin_src shell 200 | nix-build --expr 'with (import { overlays = [ (import ./.) ]; }); emacs-git' 201 | #+end_src 202 | 203 | * Community 204 | 205 | ** Matrix chat 206 | [[https://matrix.to/#/#emacs:nixos.org][Nix Emacs]] 207 | 208 | # LocalWords: EXWM NixOS 209 | # LocalWords: SRC nixpkgs builtins fetchTarball url 210 | -------------------------------------------------------------------------------- /overlays/emacs.nix: -------------------------------------------------------------------------------- 1 | self: super: 2 | let 3 | mkGitEmacs = namePrefix: jsonFile: { ... }@args: 4 | let 5 | repoMeta = super.lib.importJSON jsonFile; 6 | fetcher = 7 | if repoMeta.type == "savannah" then 8 | super.fetchgit 9 | else if repoMeta.type == "github" then 10 | super.fetchFromGitHub 11 | else 12 | throw "Unknown repository type ${repoMeta.type}!"; 13 | in 14 | builtins.foldl' 15 | (drv: fn: fn drv) 16 | super.emacs 17 | ([ 18 | 19 | (drv: drv.override ({ srcRepo = true; withXwidgets = false; } // args)) 20 | 21 | ( 22 | drv: drv.overrideAttrs ( 23 | old: { 24 | name = "${namePrefix}-${repoMeta.version}"; 25 | inherit (repoMeta) version; 26 | src = fetcher (builtins.removeAttrs repoMeta [ "type" "version" ]); 27 | 28 | patches = [ ]; 29 | 30 | # fixes segfaults that only occur on aarch64-linux (#264) 31 | configureFlags = old.configureFlags ++ 32 | super.lib.optionals (super.stdenv.isLinux && super.stdenv.isAarch64) 33 | [ "--enable-check-lisp-object-type" ]; 34 | 35 | postPatch = old.postPatch + '' 36 | substituteInPlace lisp/loadup.el \ 37 | --replace-warn '(emacs-repository-get-version)' '"${repoMeta.rev}"' \ 38 | --replace-warn '(emacs-repository-get-branch)' '"master"' 39 | '' + 40 | # XXX: Maybe remove when emacsLsp updates to use Emacs 41 | # 29. We already have logic in upstream Nixpkgs to use 42 | # a different patch for earlier major versions of Emacs, 43 | # but the major version for emacsLsp follows the format 44 | # of version YYYYMMDD, as opposed to version (say) 29. 45 | # Removing this here would also require that we don't 46 | # overwrite the patches attribute in the overlay to an 47 | # empty list since we would then expect the Nixpkgs 48 | # patch to be used. Not sure if it's better to rely on 49 | # upstream Nixpkgs since it's cumbersome to wait for 50 | # things to get merged into master. 51 | (super.lib.optionalString ((old ? NATIVE_FULL_AOT) || (old ? env.NATIVE_FULL_AOT)) 52 | (let backendPath = (super.lib.concatStringsSep " " 53 | (builtins.map (x: ''\"-B${x}\"'') ([ 54 | # Paths necessary so the JIT compiler finds its libraries: 55 | "${super.lib.getLib self.libgccjit}/lib" 56 | "${super.lib.getLib self.libgccjit}/lib/gcc" 57 | "${super.lib.getLib self.stdenv.cc.libc}/lib" 58 | ] ++ super.lib.optionals (self.stdenv.cc?cc.libgcc) [ 59 | "${super.lib.getLib self.stdenv.cc.cc.libgcc}/lib" 60 | ] ++ [ 61 | 62 | # Executable paths necessary for compilation (ld, as): 63 | "${super.lib.getBin self.stdenv.cc.cc}/bin" 64 | "${super.lib.getBin self.stdenv.cc.bintools}/bin" 65 | "${super.lib.getBin self.stdenv.cc.bintools.bintools}/bin" 66 | ] ++ super.lib.optionals (self.stdenv.hostPlatform.isDarwin && self ? apple-sdk) [ 67 | # The linker needs to know where to find libSystem on Darwin. 68 | "${self.apple-sdk.sdkroot}/usr/lib" 69 | ]))); 70 | in '' 71 | substituteInPlace lisp/emacs-lisp/comp.el --replace-warn \ 72 | "(defcustom comp-libgccjit-reproducer nil" \ 73 | "(setq native-comp-driver-options '(${backendPath})) 74 | (defcustom comp-libgccjit-reproducer nil" 75 | '')); 76 | } 77 | ) 78 | ) 79 | 80 | # reconnect pkgs to the built emacs 81 | ( 82 | drv: 83 | let 84 | result = drv.overrideAttrs (old: { 85 | passthru = old.passthru // { 86 | pkgs = self.emacsPackagesFor result; 87 | }; 88 | }); 89 | in 90 | result 91 | ) 92 | ]); 93 | 94 | emacs-git = let base = (mkGitEmacs "emacs-git" ../repos/emacs/emacs-master.json) { }; 95 | emacs = emacs-git; 96 | in 97 | base.overrideAttrs ( 98 | oa: { 99 | passthru = oa.passthru // { 100 | pkgs = oa.passthru.pkgs.overrideScope (eself: esuper: { inherit emacs; }); 101 | }; 102 | }); 103 | 104 | emacs-git-pgtk = let base = (mkGitEmacs "emacs-git-pgtk" ../repos/emacs/emacs-master.json) { withPgtk = true; }; 105 | emacs = emacs-git-pgtk; 106 | in base.overrideAttrs ( 107 | oa: { 108 | passthru = oa.passthru // { 109 | pkgs = oa.passthru.pkgs.overrideScope (eself: esuper: { inherit emacs; }); 110 | }; 111 | }); 112 | 113 | emacs-unstable = let base = (mkGitEmacs "emacs-unstable" ../repos/emacs/emacs-unstable.json) { }; 114 | emacs = emacs-unstable; 115 | in 116 | base.overrideAttrs ( 117 | oa: { 118 | passthru = oa.passthru // { 119 | pkgs = oa.passthru.pkgs.overrideScope (eself: esuper: { inherit emacs; }); 120 | }; 121 | }); 122 | 123 | emacs-unstable-pgtk = let base = (mkGitEmacs "emacs-unstable-pgtk" ../repos/emacs/emacs-unstable.json) { withPgtk = true; }; 124 | emacs = emacs-unstable-pgtk; 125 | in 126 | base.overrideAttrs ( 127 | oa: { 128 | passthru = oa.passthru // { 129 | pkgs = oa.passthru.pkgs.overrideScope (eself: esuper: { inherit emacs; }); 130 | }; 131 | }); 132 | 133 | emacs-igc = let base = (mkGitEmacs "emacs-igc" ../repos/emacs/emacs-feature_igc.json) { }; 134 | emacs = emacs-igc; 135 | in 136 | base.overrideAttrs ( 137 | oa: { 138 | buildInputs = oa.buildInputs ++ [ super.mps ]; 139 | configureFlags = oa.configureFlags ++ [ "--with-mps=yes" ]; 140 | passthru = oa.passthru // { 141 | pkgs = oa.passthru.pkgs.overrideScope (eself: esuper: { inherit emacs; }); 142 | }; 143 | }); 144 | 145 | emacs-igc-pgtk = let base = (mkGitEmacs "emacs-igc-pgtk" ../repos/emacs/emacs-feature_igc.json) { withPgtk = true; }; 146 | emacs = emacs-igc-pgtk; 147 | in 148 | base.overrideAttrs ( 149 | oa: { 150 | buildInputs = oa.buildInputs ++ [ super.mps ]; 151 | configureFlags = oa.configureFlags ++ [ "--with-mps=yes" ]; 152 | passthru = oa.passthru // { 153 | pkgs = oa.passthru.pkgs.overrideScope (eself: esuper: { inherit emacs; }); 154 | }; 155 | }); 156 | 157 | emacs-lsp = (mkGitEmacs "emacs-lsp" ../repos/emacs/emacs-lsp.json) { withTreeSitter = false; }; 158 | 159 | commercial-emacs = (mkGitEmacs "commercial-emacs" ../repos/emacs/commercial-emacs-commercial-emacs.json) { 160 | withTreeSitter = false; 161 | withNativeCompilation = false; 162 | }; 163 | 164 | emacs-git-nox = ( 165 | ( 166 | emacs-git.override { 167 | withNS = false; 168 | withX = false; 169 | withGTK3 = false; 170 | withWebP = false; 171 | } 172 | ).overrideAttrs ( 173 | oa: { 174 | name = "${oa.name}-nox"; 175 | } 176 | ) 177 | ); 178 | 179 | emacs-unstable-nox = ( 180 | ( 181 | emacs-unstable.override { 182 | withNS = false; 183 | withX = false; 184 | withGTK3 = false; 185 | withWebP = false; 186 | } 187 | ).overrideAttrs ( 188 | oa: { 189 | name = "${oa.name}-nox"; 190 | } 191 | ) 192 | ); 193 | 194 | in 195 | { 196 | inherit emacs-git emacs-unstable; 197 | 198 | inherit emacs-git-pgtk emacs-unstable-pgtk; 199 | 200 | inherit emacs-git-nox emacs-unstable-nox; 201 | 202 | inherit emacs-lsp; 203 | 204 | inherit commercial-emacs; 205 | 206 | inherit emacs-igc emacs-igc-pgtk; 207 | 208 | emacsWithPackagesFromUsePackage = import ../elisp.nix { pkgs = self; }; 209 | 210 | emacsWithPackagesFromPackageRequires = import ../packreq.nix { pkgs = self; }; 211 | 212 | } // super.lib.optionalAttrs (super.config.allowAliases or true) { 213 | emacsGcc = builtins.trace "emacsGcc has been renamed to emacs-git, please update your expression." emacs-git; 214 | emacsGitNativeComp = builtins.trace "emacsGitNativeComp has been renamed to emacs-git, please update your expression." emacs-git; 215 | emacsGitTreeSitter = builtins.trace "emacsGitTreeSitter has been renamed to emacs-git, please update your expression." emacs-git; 216 | emacsNativeComp = builtins.trace "emacsNativeComp has been renamed to emacs-unstable, please update your expression." emacs-unstable; 217 | emacsPgtkGcc = builtins.trace "emacsPgtkGcc has been renamed to emacs-pgtk, please update your expression." self.emacs-pgtk; 218 | emacsPgtkNativeComp = builtins.trace "emacsPgtkNativeComp has been renamed to emacs-pgtk, please update your expression." self.emacs-pgtk; 219 | 220 | emacsGit = builtins.trace "emacsGit has been renamed to emacs-git, please update your expression." emacs-git; 221 | emacsUnstable = builtins.trace "emacsUnstable has been renamed to emacs-unstable, please update your expression." emacs-unstable; 222 | emacsPgtk = builtins.trace "emacsPgtk has been renamed to emacs-pgtk, please update your expression." self.emacs-pgtk; 223 | emacsUnstablePgtk = builtins.trace "emacsUnstablePgtk has been renamed to emacs-unstable-pgtk, please update your expression." emacs-unstable-pgtk; 224 | emacsGitNox = builtins.trace "emacsGitNox has been renamed to emacs-git-nox, please update your expression." emacs-git-nox; 225 | emacsUnstableNox = builtins.trace "emacsUnstableNox has been renamed to emacs-unstable-nox, please update your expression." emacs-unstable-nox; 226 | emacsLsp = builtins.trace "emacsLsp has been renamed to emacs-lsp, please update your expression." emacs-lsp; 227 | } 228 | -------------------------------------------------------------------------------- /repos/fromElisp/default.nix: -------------------------------------------------------------------------------- 1 | # WARNING: This file was automatically imported from 2 | # https://github.com/talyz/fromElisp. Don't make any changes to it 3 | # locally - they will be discarded on update! 4 | 5 | { lib ? pkgs.lib 6 | , pkgs ? import { } 7 | , commentMaxLength ? 300 8 | , stringMaxLength ? 3000 9 | , characterMaxLength ? 50 10 | , integerMaxLength ? 50 11 | , floatMaxLength ? 50 12 | , boolVectorMaxLength ? 50 13 | , symbolMaxLength ? 50 14 | , orgModeBabelCodeBlockHeaderMaxLength ? 200 15 | , orgModeBabelCodeBlockArgMaxLength ? 30 16 | }: 17 | 18 | let 19 | inherit (lib) 20 | substring length replaceStrings genList head const max elem seq 21 | stringLength stringToCharacters tail elemAt isList 22 | isString toLower 23 | ; 24 | inherit (builtins) match foldl' filter fromJSON; 25 | 26 | # Modulo 27 | mod = i: d: i - ((i / d) * d); 28 | 29 | isWhitespace = lib.flip elem [ " " "\t" "\r" ]; 30 | 31 | # Create a matcher from a regex string and maximum length. A 32 | # matcher takes a string and returns the first match produced by 33 | # running its regex on it, or null if the match is unsuccessful, 34 | # but only as far in as specified by maxLength. 35 | mkMatcher = regex: maxLength: 36 | string: 37 | let 38 | substr = substring 0 maxLength string; 39 | matched = match regex substr; 40 | in 41 | if matched != null then head matched else null; 42 | 43 | removeStrings = stringsToRemove: let 44 | len = length stringsToRemove; 45 | listOfNullStrings = genList (const "") len; 46 | in replaceStrings stringsToRemove listOfNullStrings; 47 | 48 | # Split a string of elisp into individual tokens and add useful 49 | # metadata. 50 | tokenizeElisp' = let 51 | # These are the only characters that can not be unescaped in a 52 | # symbol name. We match the inverse of these to get the actual 53 | # symbol characters and use them to differentiate between 54 | # symbols and tokens that could potentially look like symbols, 55 | # such as numbers. Due to the leading bracket, this has to be 56 | # placed _first_ inside a bracket expression. 57 | notInSymbol = '']["'`,#;\\()[:space:][:cntrl:]''; 58 | 59 | matchComment = mkMatcher "(;[^\n]*).*" commentMaxLength; 60 | 61 | matchString = mkMatcher ''("([^"\\]|\\.)*").*'' stringMaxLength; 62 | 63 | matchCharacter = mkMatcher ''([?]((\\[sSHMAC]-)|\\\^)*(([^][\\()]|\\[][\\()])|\\[^^SHMACNuUx0-7]|\\[uU][[:digit:]a-fA-F]+|\\x[[:digit:]a-fA-F]*|\\[0-7]{1,3}|\\N\{[^}]+}))([${notInSymbol}?]|$).*'' characterMaxLength; 64 | 65 | matchNonBase10Integer = mkMatcher ''(#([BbOoXx]|[[:digit:]]{1,2}r)[[:digit:]a-fA-F]+)([${notInSymbol}]|$).*'' integerMaxLength; 66 | 67 | matchInteger = mkMatcher ''([+-]?[[:digit:]]+[.]?)([${notInSymbol}]|$).*'' integerMaxLength; 68 | 69 | matchBoolVector = mkMatcher ''(#&[[:digit:]]+"([^"\\]|\\.)*").*'' boolVectorMaxLength; 70 | 71 | matchFloat = mkMatcher ''([+-]?([[:digit:]]*[.][[:digit:]]+|([[:digit:]]*[.])?[[:digit:]]+e([+-]?[[:digit:]]+|[+](INF|NaN))))([${notInSymbol}]|$).*'' floatMaxLength; 72 | 73 | matchDot = mkMatcher ''([.])([${notInSymbol}]|$).*'' 2; 74 | 75 | matchFunction = throw "matchFunction: Not implemented"; 76 | 77 | # Symbols can contain pretty much any characters - the general 78 | # rule is that if nothing else matches, it's a symbol, so we 79 | # should be pretty generous here and match for symbols last. See 80 | # https://www.gnu.org/software/emacs/manual/html_node/elisp/Symbol-Type.html 81 | matchSymbol = 82 | let 83 | symbolChar = ''([^${notInSymbol}]|\\.)''; 84 | in mkMatcher ''(${symbolChar}+)([${notInSymbol}]|$).*'' symbolMaxLength; 85 | 86 | 87 | isDigit = lib.flip elem [ "+" "-" "." "0" "1" "2" "3" "4" "5" "6" "7" "8" "9" ]; 88 | 89 | maxTokenLength = foldl' max 0 [ 90 | commentMaxLength 91 | stringMaxLength 92 | characterMaxLength 93 | integerMaxLength 94 | floatMaxLength 95 | boolVectorMaxLength 96 | symbolMaxLength 97 | ]; 98 | in { elisp, startLineNumber ? 1 }: 99 | let 100 | # Fold over all the characters in a string, checking for 101 | # matching tokens. 102 | # 103 | # The implementation is a bit obtuse, for optimization reasons: 104 | # nix doesn't have tail-call optimization, thus a strict fold, 105 | # which should essentially force a limited version of tco when 106 | # iterating a list, is our best alternative. 107 | # 108 | # The string read from is split into a list of its constituent 109 | # characters, which is then folded over. Each character is then 110 | # used to determine a likely matching regex "matcher" to run on 111 | # the string, starting at the position of the aforementioned 112 | # character. When an appropriate matcher has been found and run 113 | # successfully on the string, its result is added to 114 | # `state.acc`, a list of all matched tokens. The length of the 115 | # matched token is determined and passed on to the following 116 | # iteration through `state.skip`. If `state.skip` is positive, 117 | # nothing will be done in the current iteration, except 118 | # decrementing `state.skip` for the next one: this skips the 119 | # characters we've already matched. At each iteration, 120 | # `state.pos` is also incremented, to keep track of the current 121 | # string position. 122 | # 123 | # The order of the matches is significant - matchSymbol will, 124 | # for example, also match numbers and characters, so we check 125 | # for symbols last. 126 | readToken = state: char: 127 | let 128 | rest = substring state.pos maxTokenLength elisp; 129 | comment = matchComment rest; 130 | character = matchCharacter rest; 131 | nonBase10Integer = matchNonBase10Integer rest; 132 | integer = matchInteger rest; 133 | float = matchFloat rest; 134 | function = matchFunction rest; 135 | boolVector = matchBoolVector rest; 136 | string = matchString rest; 137 | dot = matchDot rest; 138 | symbol = matchSymbol rest; 139 | in 140 | if state.skip > 0 then 141 | state // { 142 | pos = state.pos + 1; 143 | skip = state.skip - 1; 144 | line = if char == "\n" then state.line + 1 else state.line; 145 | } 146 | else if char == "\n" then 147 | let 148 | newState = { 149 | pos = state.pos + 1; 150 | line = state.line + 1; 151 | }; 152 | in 153 | state // ( 154 | # Force evaluation of old state every 1000 lines. Nix 155 | # doesn't have a modulo builtin, so we have to save 156 | # the result of an integer division and compare 157 | # between runs. 158 | if mod state.line 1000 == 0 then 159 | seq state.acc newState 160 | else 161 | newState 162 | ) 163 | else if isWhitespace char then 164 | state // { 165 | pos = state.pos + 1; 166 | inherit (state) line; 167 | } 168 | else if char == ";" then 169 | if comment != null then 170 | state // { 171 | pos = state.pos + 1; 172 | skip = (stringLength comment) - 1; 173 | } 174 | else throw "Unrecognized token on line ${toString state.line}: ${rest}" 175 | else if char == "(" then 176 | state // { 177 | acc = state.acc ++ [{ type = "openParen"; value = "("; inherit (state) line; }]; 178 | pos = state.pos + 1; 179 | } 180 | else if char == ")" then 181 | state // { 182 | acc = state.acc ++ [{ type = "closeParen"; value = ")"; inherit (state) line; }]; 183 | pos = state.pos + 1; 184 | } 185 | else if char == "[" then 186 | state // { 187 | acc = state.acc ++ [{ type = "openBracket"; value = "["; inherit (state) line; }]; 188 | pos = state.pos + 1; 189 | } 190 | else if char == "]" then 191 | state // { 192 | acc = state.acc ++ [{ type = "closeBracket"; value = "]"; inherit (state) line; }]; 193 | pos = state.pos + 1; 194 | } 195 | else if char == "'" then 196 | state // { 197 | acc = state.acc ++ [{ type = "quote"; value = "'"; inherit (state) line; }]; 198 | pos = state.pos + 1; 199 | } 200 | else if char == ''"'' then 201 | if string != null then 202 | state // { 203 | acc = state.acc ++ [{ type = "string"; value = string; inherit (state) line; }]; 204 | pos = state.pos + 1; 205 | skip = (stringLength string) - 1; 206 | } 207 | else throw "Unrecognized token on line ${toString state.line}: ${rest}" 208 | else if char == "#" then 209 | let nextChar = substring 1 1 rest; 210 | in 211 | if nextChar == "'" then 212 | state // { 213 | acc = state.acc ++ [{ type = "function"; value = "#'"; inherit (state) line; }]; 214 | pos = state.pos + 1; 215 | skip = 1; 216 | } 217 | else if nextChar == "&" then 218 | if boolVector != null then 219 | state // { 220 | acc = state.acc ++ [{ type = "boolVector"; value = boolVector; inherit (state) line; }]; 221 | pos = state.pos + 1; 222 | skip = (stringLength boolVector) - 1; 223 | } 224 | else throw "Unrecognized token on line ${toString state.line}: ${rest}" 225 | else if nextChar == "s" then 226 | if substring 2 1 rest == "(" then 227 | state // { 228 | acc = state.acc ++ [{ type = "record"; value = "#s"; inherit (state) line; }]; 229 | pos = state.pos + 1; 230 | skip = 1; 231 | } 232 | else throw "List must follow #s in record on line ${toString state.line}: ${rest}" 233 | else if nextChar == "[" then 234 | state // { 235 | acc = state.acc ++ [{ type = "byteCode"; value = "#"; inherit (state) line; }]; 236 | pos = state.pos + 1; 237 | } 238 | else if nonBase10Integer != null then 239 | state // { 240 | acc = state.acc ++ [{ type = "nonBase10Integer"; value = nonBase10Integer; inherit (state) line; }]; 241 | pos = state.pos + 1; 242 | skip = (stringLength nonBase10Integer) - 1; 243 | } 244 | else throw "Unrecognized token on line ${toString state.line}: ${rest}" 245 | else if isDigit char then 246 | if integer != null then 247 | state // { 248 | acc = state.acc ++ [{ type = "integer"; value = integer; inherit (state) line; }]; 249 | pos = state.pos + 1; 250 | skip = (stringLength integer) - 1; 251 | } 252 | else if float != null then 253 | state // { 254 | acc = state.acc ++ [{ type = "float"; value = float; inherit (state) line; }]; 255 | pos = state.pos + 1; 256 | skip = (stringLength float) - 1; 257 | } 258 | else if dot != null then 259 | state // { 260 | acc = state.acc ++ [{ type = "dot"; value = dot; inherit (state) line; }]; 261 | pos = state.pos + 1; 262 | skip = (stringLength dot) - 1; 263 | } 264 | else if symbol != null then 265 | state // { 266 | acc = state.acc ++ [{ type = "symbol"; value = symbol; inherit (state) line; }]; 267 | pos = state.pos + 1; 268 | skip = (stringLength symbol) - 1; 269 | } 270 | else throw "Unrecognized token on line ${toString state.line}: ${rest}" 271 | else if char == "?" then 272 | if character != null then 273 | state // { 274 | acc = state.acc ++ [{ type = "character"; value = character; inherit (state) line; }]; 275 | pos = state.pos + 1; 276 | skip = (stringLength character) - 1; 277 | } 278 | else throw "Unrecognized token on line ${toString state.line}: ${rest}" 279 | else if char == "`" then 280 | state // { 281 | acc = state.acc ++ [{ type = "backquote"; value = "`"; inherit (state) line; }]; 282 | pos = state.pos + 1; 283 | } 284 | else if char == "," then 285 | if substring 1 1 rest == "@" then 286 | state // { 287 | acc = state.acc ++ [{ type = "slice"; value = ",@"; inherit (state) line; }]; 288 | skip = 1; 289 | pos = state.pos + 1; 290 | } 291 | else 292 | state // { 293 | acc = state.acc ++ [{ type = "expand"; value = ","; inherit (state) line; }]; 294 | pos = state.pos + 1; 295 | } 296 | else if symbol != null then 297 | state // { 298 | acc = state.acc ++ [{ type = "symbol"; value = symbol; inherit (state) line; }]; 299 | pos = state.pos + 1; 300 | skip = (stringLength symbol) - 1; 301 | } 302 | else 303 | throw "Unrecognized token on line ${toString state.line}: ${rest}"; 304 | in (builtins.foldl' readToken { 305 | acc = []; 306 | pos = 0; 307 | skip = 0; 308 | line = startLineNumber; 309 | } (stringToCharacters elisp)).acc; 310 | 311 | tokenizeElisp = elisp: 312 | tokenizeElisp' { inherit elisp; }; 313 | 314 | # Produce an AST from a list of tokens produced by `tokenizeElisp`. 315 | parseElisp' = let 316 | removeIntDelimiter = removeStrings ["+" "."]; 317 | removePlus = removeStrings ["+"]; 318 | removeMinus = removeStrings ["-"]; 319 | in tokens: 320 | let 321 | # Convert literal value tokens in a flat list to their 322 | # corresponding nix representation. 323 | parseValues = tokens: 324 | map (token: 325 | if token.type == "string" then 326 | token // { 327 | value = substring 1 (stringLength token.value - 2) token.value; 328 | } 329 | else if token.type == "integer" then 330 | token // { 331 | value = fromJSON (removeIntDelimiter token.value); 332 | } 333 | else if token.type == "symbol" && token.value == "t" then 334 | token // { 335 | value = true; 336 | } 337 | else if token.type == "float" then 338 | let 339 | initial = head (match "([+-]?([[:digit:]]*[.])?[[:digit:]]+(e([+-]?[[:digit:]]+|[+](INF|NaN)))?)" token.value); 340 | isSpecial = (match "(.+(e[+](INF|NaN)))" initial) != null; 341 | withoutPlus = removePlus initial; 342 | withPrefix = 343 | if substring 0 1 withoutPlus == "." then 344 | "0" + withoutPlus 345 | else if substring 0 2 withoutPlus == "-." then 346 | "-0" + removeMinus withoutPlus 347 | else 348 | withoutPlus; 349 | in 350 | if !isSpecial && withPrefix != null then 351 | token // { 352 | value = fromJSON withPrefix; 353 | } 354 | else 355 | token 356 | else 357 | token 358 | ) tokens; 359 | 360 | # Convert pairs of opening and closing tokens to their 361 | # respective collection types, i.e. lists and vectors. Also, 362 | # normalize the forms of nil, which can be written as either 363 | # `nil` or `()`, to empty lists. 364 | # 365 | # For performance reasons, this is implemented as a fold over 366 | # the list of tokens, rather than as a recursive function. To 367 | # keep track of list depth when sublists are parsed, a list, 368 | # `state.acc`, is used as a stack. When entering a sublist, an 369 | # empty list is pushed to `state.acc`, and items in the sublist 370 | # are subsequently added to this list. When exiting the list, 371 | # `state.acc` is popped and the completed list is added to the 372 | # new head of `state.acc`, i.e. the outer list, which we were 373 | # parsing before entering the sublist. 374 | # 375 | # Evaluation of old state is forced with `seq` in a few places, 376 | # because nix otherwise keeps it around, eventually resulting in 377 | # a stack overflow. 378 | parseCollections = tokens: 379 | let 380 | parseToken = state: token: 381 | let 382 | openColl = if token.type == "openParen" then "list" else if token.type == "openBracket" then "vector" else null; 383 | closeColl = if token.type == "closeParen" then "list" else if token.type == "closeBracket" then "vector" else null; 384 | in 385 | if openColl != null then 386 | state // { 387 | acc = [ [] ] ++ seq (head state.acc) state.acc; 388 | inColl = [ openColl ] ++ state.inColl; 389 | depth = state.depth + 1; 390 | line = [ token.line ] ++ state.line; 391 | } 392 | else if closeColl != null then 393 | if (head state.inColl) == closeColl then 394 | let 395 | outerColl = elemAt state.acc 1; 396 | currColl = { 397 | type = closeColl; 398 | value = head state.acc; 399 | line = head state.line; 400 | inherit (state) depth; 401 | }; 402 | rest = tail (tail state.acc); 403 | in 404 | state // seq state.acc { 405 | acc = [ (outerColl ++ [ currColl ]) ] ++ rest; 406 | inColl = tail state.inColl; 407 | depth = state.depth - 1; 408 | line = tail state.line; 409 | } 410 | else 411 | throw "Unmatched ${token.type} on line ${toString token.line}" 412 | else if token.type == "symbol" && token.value == "nil" then 413 | let 414 | currColl = head state.acc; 415 | rest = tail state.acc; 416 | emptyList = { 417 | type = "list"; 418 | depth = state.depth + 1; 419 | value = []; 420 | }; 421 | in 422 | state // seq currColl { acc = [ (currColl ++ [ emptyList ]) ] ++ rest; } 423 | else 424 | let 425 | currColl = head state.acc; 426 | rest = tail state.acc; 427 | in 428 | state // seq currColl { acc = [ (currColl ++ [ token ]) ] ++ rest; }; 429 | in 430 | head (builtins.foldl' parseToken { acc = [ [] ]; inColl = [ null ]; depth = -1; line = []; } tokens).acc; 431 | 432 | # Handle dotted pair notation, a syntax where the car and cdr 433 | # are represented explicitly. See 434 | # https://www.gnu.org/software/emacs/manual/html_node/elisp/Dotted-Pair-Notation.html#Dotted-Pair-Notation 435 | # for more info. 436 | # 437 | # This mainly entails handling lists that are the cdrs of a 438 | # dotted pairs, concatenating the lexically distinct lists into 439 | # the logical list they actually represent. 440 | # 441 | # For example: 442 | # (a . (b . (c . nil))) -> (a b c) 443 | parseDots = tokens: 444 | let 445 | parseToken = state: token: 446 | if token.type == "dot" then 447 | if state.inList then 448 | state // { 449 | dotted = true; 450 | depthReduction = state.depthReduction + 1; 451 | } 452 | else 453 | throw ''"Dotted pair notation"-dot outside list on line ${toString token.line}'' 454 | else if isList token.value then 455 | let 456 | collectionContents = foldl' parseToken { 457 | acc = []; 458 | dotted = false; 459 | inList = token.type == "list"; 460 | inherit (state) depthReduction; 461 | } token.value; 462 | in 463 | state // { 464 | acc = state.acc ++ ( 465 | if state.dotted then 466 | collectionContents.acc 467 | else 468 | [ 469 | (token // { 470 | value = collectionContents.acc; 471 | depth = token.depth - state.depthReduction; 472 | }) 473 | ] 474 | ); 475 | dotted = false; 476 | } 477 | else 478 | state // { 479 | acc = state.acc ++ [token]; 480 | }; 481 | in 482 | (foldl' parseToken { acc = []; dotted = false; inList = false; depthReduction = 0; } tokens).acc; 483 | 484 | parseQuotes = let 485 | isQuote = lib.flip elem [ "quote" "expand" "slice" "backquote" "function" "record" "byteCode" ]; 486 | in tokens: 487 | let 488 | parseToken = state: token': 489 | let 490 | token = 491 | if isList token'.value then 492 | token' // { 493 | value = (foldl' parseToken { acc = []; quotes = []; } token'.value).acc; 494 | } 495 | else 496 | token'; 497 | in 498 | if isQuote token.type then 499 | state // { 500 | quotes = [ token ] ++ state.quotes; 501 | } 502 | else if state.quotes != [] then 503 | let 504 | quote = value: token: 505 | token // { 506 | inherit value; 507 | }; 508 | quotedValue = foldl' quote token state.quotes; 509 | in 510 | state // { 511 | acc = state.acc ++ [ quotedValue ]; 512 | quotes = []; 513 | } 514 | else 515 | state // { 516 | acc = state.acc ++ [ token ]; 517 | }; 518 | in 519 | (foldl' parseToken { acc = []; quotes = []; } tokens).acc; 520 | in 521 | parseQuotes (parseDots (parseCollections (parseValues tokens))); 522 | 523 | parseElisp = elisp: 524 | parseElisp' (tokenizeElisp elisp); 525 | 526 | fromElisp' = ast: 527 | let 528 | readObject = object: 529 | if isList object.value then 530 | map readObject object.value 531 | else if object.type == "quote" then 532 | ["quote" (readObject object.value)] 533 | else if object.type == "backquote" then 534 | ["`" (readObject object.value)] 535 | else if object.type == "expand" then 536 | ["," (readObject object.value)] 537 | else if object.type == "slice" then 538 | [",@" (readObject object.value)] 539 | else if object.type == "function" then 540 | ["#'" (readObject object.value)] 541 | else if object.type == "byteCode" then 542 | ["#"] ++ (readObject object.value) 543 | else if object.type == "record" then 544 | ["#s"] ++ (readObject object.value) 545 | else 546 | object.value; 547 | in 548 | map readObject ast; 549 | 550 | fromElisp = elisp: 551 | fromElisp' (parseElisp elisp); 552 | 553 | # Parse an Org mode babel text and return a list of all code blocks 554 | # with metadata. 555 | # 556 | # The general operation is similar to tokenizeElisp', so check its 557 | # documentation for a more in-depth description. 558 | # 559 | # As in tokenizeElisp', the string read from is split into a list of 560 | # its constituent characters, which is then folded over. Each 561 | # character is then used to determine whether we should try to run a 562 | # match for a `#+begin_src` header or `#+end_src` footer, starting 563 | # at the position of the aforementioned character. These matches 564 | # should only be attempted if the current character is `#` and the 565 | # line has nothing but whitespace before it (noted by 566 | # `state.leadingWhitespace`). 567 | # 568 | # When an appropriate match for a header has been found, its 569 | # arguments are further parsed and the result is put into the code 570 | # block's `flags` attribute. The subsequent characters are added to 571 | # the code block's `body` attribute, until a footer is successfully 572 | # matched and the block is added to the list of parsed blocks, 573 | # `state.acc`. 574 | parseOrgModeBabel = let 575 | matchBeginCodeBlock = mkMatcher "(#[+][bB][eE][gG][iI][nN]_[sS][rR][cC])([[:space:]]+).*" orgModeBabelCodeBlockHeaderMaxLength; 576 | matchHeader = mkMatcher "(#[+][hH][eE][aA][dD][eE][rR][sS]?:)([[:space:]]+).*" orgModeBabelCodeBlockHeaderMaxLength; 577 | matchEndCodeBlock = mkMatcher "(#[+][eE][nN][dD]_[sS][rR][cC][^\n]*).*" orgModeBabelCodeBlockHeaderMaxLength; 578 | 579 | matchBeginCodeBlockLang = match "([[:blank:]]*)([[:alnum:]][[:alnum:]-]*).*"; 580 | matchBeginCodeBlockFlags = mkMatcher "([^\n]*[\n]).*" orgModeBabelCodeBlockHeaderMaxLength; 581 | 582 | isItem = lib.flip elem [ ":" "-" "+" ]; 583 | 584 | in text: 585 | let 586 | parseToken = state: char: 587 | let 588 | rest = substring state.pos orgModeBabelCodeBlockHeaderMaxLength text; 589 | beginCodeBlock = matchBeginCodeBlock rest; 590 | header = matchHeader rest; 591 | endCodeBlock = matchEndCodeBlock rest; 592 | language = matchBeginCodeBlockLang rest; 593 | flags = matchBeginCodeBlockFlags rest; 594 | 595 | force = expr: seq state.pos (seq state.line expr); 596 | in 597 | if state.skip > 0 then 598 | state // force { 599 | pos = state.pos + 1; 600 | skip = state.skip - 1; 601 | line = if char == "\n" then state.line + 1 else state.line; 602 | leadingWhitespace = char == "\n" || (state.leadingWhitespace && isWhitespace char); 603 | } 604 | else if char == "#" && state.leadingWhitespace && !state.readBody && beginCodeBlock != null then 605 | state // { 606 | pos = state.pos + 1; 607 | skip = (stringLength beginCodeBlock) - 1; 608 | leadingWhitespace = false; 609 | readLanguage = true; 610 | } 611 | else if char == "#" && state.leadingWhitespace && !state.readBody && header != null then 612 | state // { 613 | pos = state.pos + 1; 614 | skip = (stringLength header) - 1; 615 | leadingWhitespace = false; 616 | readFlags = true; 617 | } 618 | else if state.readLanguage then 619 | if language != null then 620 | state // { 621 | block = state.block // { 622 | language = elemAt language 1; 623 | }; 624 | pos = state.pos + 1; 625 | skip = (foldl' (total: string: total + (stringLength string)) 0 language) - 1; 626 | leadingWhitespace = false; 627 | readLanguage = false; 628 | readFlags = true; 629 | readBody = true; 630 | } 631 | else throw "Language missing or invalid for code block on line ${toString state.line}!" 632 | else if state.readFlags then 633 | if flags != null then 634 | let 635 | parseFlag = state: item: 636 | let 637 | prefix = if isString item then substring 0 1 item else null; 638 | in 639 | if isItem prefix then 640 | state // { 641 | acc = state.acc // { ${item} = true; }; 642 | flag = item; 643 | } 644 | else if state.flag != null then 645 | state // { 646 | acc = state.acc // { ${state.flag} = item; }; 647 | flag = null; 648 | } 649 | else 650 | state; 651 | in 652 | state // { 653 | block = state.block // { 654 | flags = 655 | (foldl' 656 | parseFlag 657 | { acc = state.block.flags; 658 | flag = null; 659 | inherit (state) line; 660 | } 661 | (fromElisp flags)).acc; 662 | startLineNumber = state.line + 1; 663 | }; 664 | pos = state.pos + 1; 665 | skip = (stringLength flags) - 1; 666 | line = if char == "\n" then state.line + 1 else state.line; 667 | leadingWhitespace = char == "\n"; 668 | readFlags = false; 669 | } 670 | else throw "Arguments malformed for code block on line ${toString state.line}!" 671 | else if char == "#" && state.leadingWhitespace && endCodeBlock != null then 672 | state // { 673 | acc = state.acc ++ [ state.block ]; 674 | block = { 675 | language = null; 676 | body = ""; 677 | flags = {}; 678 | }; 679 | pos = state.pos + 1; 680 | skip = (stringLength endCodeBlock) - 1; 681 | leadingWhitespace = false; 682 | readBody = false; 683 | } 684 | else if state.readBody then 685 | let 686 | newState = { 687 | block = state.block // { 688 | body = state.block.body + char; 689 | }; 690 | pos = state.pos + 1; 691 | line = if char == "\n" then state.line + 1 else state.line; 692 | leadingWhitespace = char == "\n" || (state.leadingWhitespace && isWhitespace char); 693 | }; 694 | in 695 | if mod state.pos 100 == 0 then 696 | state // seq state.block.body (force newState) 697 | else 698 | state // newState 699 | else 700 | state // force { 701 | pos = state.pos + 1; 702 | line = if char == "\n" then state.line + 1 else state.line; 703 | leadingWhitespace = char == "\n" || (state.leadingWhitespace && isWhitespace char); 704 | }; 705 | in 706 | (foldl' 707 | parseToken 708 | { acc = []; 709 | pos = 0; 710 | skip = 0; 711 | line = 1; 712 | block = { 713 | language = null; 714 | body = ""; 715 | flags = {}; 716 | }; 717 | leadingWhitespace = true; 718 | readLanguage = false; 719 | readFlags = false; 720 | readBody = false; 721 | } 722 | (stringToCharacters text)).acc; 723 | 724 | # Run tokenizeElisp' on all Elisp code blocks (with `:tangle yes` 725 | # set) from an Org mode babel text. If the block doesn't have a 726 | # `tangle` attribute, it's determined by `defaultArgs`. 727 | tokenizeOrgModeBabelElisp' = let 728 | isElisp = lib.flip elem [ "elisp" "emacs-lisp" ]; 729 | doTangle = lib.flip elem [ "yes" ''"yes"'' ]; 730 | in defaultArgs: text: 731 | let 732 | codeBlocks = 733 | filter 734 | (block: 735 | let 736 | tangle = toLower (block.flags.":tangle" or defaultArgs.":tangle" or "no"); 737 | language = toLower block.language; 738 | in isElisp language && doTangle tangle) 739 | (parseOrgModeBabel text); 740 | in 741 | foldl' 742 | (result: codeBlock: 743 | result ++ (tokenizeElisp' { 744 | elisp = codeBlock.body; 745 | inherit (codeBlock) startLineNumber; 746 | }) 747 | ) 748 | [] 749 | codeBlocks; 750 | 751 | tokenizeOrgModeBabelElisp = 752 | tokenizeOrgModeBabelElisp' { 753 | ":tangle" = "no"; 754 | }; 755 | 756 | parseOrgModeBabelElisp' = defaultArgs: text: 757 | parseElisp' (tokenizeOrgModeBabelElisp' defaultArgs text); 758 | 759 | parseOrgModeBabelElisp = text: 760 | parseElisp' (tokenizeOrgModeBabelElisp text); 761 | 762 | fromOrgModeBabelElisp' = defaultArgs: text: 763 | fromElisp' (parseOrgModeBabelElisp' defaultArgs text); 764 | 765 | fromOrgModeBabelElisp = text: 766 | fromElisp' (parseOrgModeBabelElisp text); 767 | 768 | in 769 | { 770 | inherit tokenizeElisp parseElisp fromElisp; 771 | inherit tokenizeElisp' parseElisp' fromElisp'; 772 | inherit tokenizeOrgModeBabelElisp parseOrgModeBabelElisp fromOrgModeBabelElisp; 773 | inherit tokenizeOrgModeBabelElisp' parseOrgModeBabelElisp' fromOrgModeBabelElisp'; 774 | } 775 | --------------------------------------------------------------------------------