Add impure Racket translator

This commit is contained in:
Brian Leung 2022-09-14 04:15:09 -07:00
parent a6502bd52e
commit c17d995e8d
No known key found for this signature in database
GPG Key ID: 2D86D6A94C8BB3B1
3 changed files with 248 additions and 0 deletions

18
examples/racket/flake.nix Normal file
View File

@ -0,0 +1,18 @@
{
inputs = {
dream2nix.url = "github:nix-community/dream2nix";
goblins.url = "gitlab:leungbk/goblins";
goblins.flake = false;
};
outputs = {
self,
dream2nix,
goblins,
} @ inp: (dream2nix.lib.makeFlakeOutputs {
systems = ["x86_64-linux"];
config.projectRoot = ./.;
source = goblins;
});
}

View File

@ -0,0 +1,80 @@
{
dlib,
lib,
...
}: let
l = lib // builtins;
in {
type = "impure";
# A derivation which outputs a single executable at `$out`.
# The executable will be called by dream2nix for translation
# The input format is specified in /specifications/translator-call-example.json.
# The first arg `$1` will be a json file containing the input parameters
# like defined in /src/specifications/translator-call-example.json and the
# additional arguments required according to extraArgs
#
# The program is expected to create a file at the location specified
# by the input parameter `outFile`.
# The output file must contain the dream lock data encoded as json.
# See /src/specifications/dream-lock-example.json
translateBin = {
# dream2nix utils
utils,
# nixpkgs dependencies
bash,
coreutils,
fetchurl,
jq,
nix,
racket,
runCommandLocal,
writeScriptBin,
...
}: let
pruned-racket-catalog = let
src = fetchurl {
url = "https://github.com/nix-community/pruned-racket-catalog/tarball/9f11e5ea5765c8a732c5e3129ca2b71237ae2bac";
sha256 = "sha256-/n30lailqSndoqPGWcFquCpQWVQcciMiypXYLhNmFUo=";
};
in
runCommandLocal "pruned-racket-catalog" {} ''
mkdir $out
cd $out
tar --strip-components 1 -xf ${src}
'';
in
utils.writePureShellScript
[
bash
coreutils
jq
nix
racket
]
''
# according to the spec, the translator reads the input from a json file
jsonInput=$1
# read the json input
outputFile=$(realpath -m $(jq '.outputFile' -c -r $jsonInput))
source=$(jq '.source' -c -r $jsonInput)
relPath=$(jq '.project.relPath' -c -r $jsonInput)
name=$(jq '.project.name' -c -r $jsonInput)
export RACKET_OUTPUT_FILE=$outputFile
export RACKET_SOURCE=$source
export RACKET_RELPATH=$relPath
export RACKET_PKG_MAYBE_NAME=$name
racket -e '(require (file "${./generate-dream-lock.rkt}")) (generate-dream-lock "${pruned-racket-catalog}/pkgs-all")'
'';
# If the translator requires additional arguments, specify them here.
# When users run the CLI, they will be asked to specify these arguments.
# There are only two types of arguments:
# - string argument (type = "argument")
# - boolean flag (type = "flag")
# String arguments contain a default value and examples. Flags do not.
extraArgs = {};
}

View File

@ -0,0 +1,150 @@
#lang racket/base
(require json)
(require racket/file)
(require racket/match)
(require racket/list)
(require racket/set)
(require racket/string)
(require setup/getinfo)
(provide generate-dream-lock)
;; XXX: We presently end up doing multiple DFSes in the course of
;; generating multiple dream-lock.json files for foo, foo-lib,
;; foo-test: the issue is that generating the foo dream-lock requires
;; that we traverse foo-lib, and that subsequently generating the
;; foo-lib dream-lock requires repeating the same traversal of foo-lib. How can this be avoided?
(define (generate-dream-lock pkgs-all-path)
(letrec ([src-path (getenv "RACKET_SOURCE")]
[rel-path (getenv "RACKET_RELPATH")]
[package-path (simplify-path (cleanse-path (build-path src-path (if (string=? rel-path "")
'same
rel-path))))]
[parent-path (simplify-path (cleanse-path (build-path package-path 'up)))]
[package-name (if (string=? rel-path "")
(getenv "RACKET_PKG_MAYBE_NAME")
(path->string
(match/values (split-path package-path)
((_base subdir _must-be-dir?) subdir))))]
[pkgs-all (with-input-from-file pkgs-all-path read)]
[pkg-in-stdlib? (lambda (pkg-name)
(or ;; Some people add racket itself as a dependency for some reason
(string=? pkg-name "racket")
(ormap (lambda (tag)
;; XXX: would prefer to use memq, but tag is mutable for some reason
(member tag '("main-distribution" "main-tests")))
(hash-ref (hash-ref pkgs-all pkg-name) 'tags))))]
[dep-alist-from-catalog (hash-map pkgs-all
(match-lambda**
[(name (hash-table ('dependencies dependencies)))
(let ([external-deps (filter-not pkg-in-stdlib? dependencies)])
(cons name external-deps))]))]
[compute-overridden-dep-lists
(lambda (name dir)
(let ([info-procedure (get-info/full dir)])
(and info-procedure
(cons name
(remove-duplicates
(filter-not pkg-in-stdlib?
(map (match-lambda
[(or (cons pkg-name _) pkg-name)
pkg-name])
(append (with-handlers ([exn:fail? (lambda (_) '())])
(info-procedure 'deps))
(with-handlers ([exn:fail? (lambda (_) '())])
(info-procedure 'build-deps))))))))))]
[dep-list-overrides
;; XXX: this probably doesn't capture every case since
;; Racket doesn't seem to enforce much structure in a
;; multi-package repo, but it accounts for the only cases
;; that a sane person would choose
(if (string=? rel-path "")
(list (compute-overridden-dep-lists package-name package-path))
(let* ([sibling-paths (filter directory-exists?
(directory-list parent-path #:build? #t))]
[names-of-sibling-paths (map (lambda (p)
;; XXX: maybe not very DRY
(path->string
(match/values (split-path p)
((_base dir-fragment _must-be-dir?) dir-fragment))))
sibling-paths)])
(filter-map compute-overridden-dep-lists
names-of-sibling-paths
sibling-paths)))]
[names-of-overridden-packages (apply set (map car dep-list-overrides))]
[graph (make-immutable-hash (append dep-alist-from-catalog
dep-list-overrides))]
;; TODO: no effort is made to handle cycles right now
[dfs (lambda (u dependency-subgraph)
(if (hash-has-key? dependency-subgraph u)
dependency-subgraph
(let ([destinations (hash-ref graph u)])
(foldl dfs
(hash-set dependency-subgraph u destinations)
destinations))))]
[dependency-subgraph (dfs package-name (make-immutable-hash))]
[generic (make-immutable-hash
`((subsystem . "racket")
(location . ,rel-path)
(sourcesAggregatedHash . ,(json-null))
(defaultPackage . ,package-name)
(packages . ,(make-immutable-hash `((,(string->symbol package-name) . "0.0.0"))))))]
[sources-from-catalog
(hash-map pkgs-all
(match-lambda**
[(name (hash-table
('versions
(hash-table
('default
(hash-table
('source_url url)))))
('checksum rev)))
(let* ([source-with-removed-http-or-git-double-slash (regexp-replace #rx"^(?:git|http)://" url "https://")]
[left-trimmed-source (string-trim source-with-removed-http-or-git-double-slash "git+" #:right? #f)]
[maybe-match-path (regexp-match #rx"\\?path=([^#]+)" left-trimmed-source)]
[trimmed-source (regexp-replace #rx"(?:/tree/.+)?(?:\\?path=.+)?$" left-trimmed-source "")])
(cons (string->symbol name)
(make-immutable-hash
`((0.0.0 . ,(make-immutable-hash
(append (match maybe-match-path
[(list _match dir)
`((dir . ,(regexp-replace* #rx"%2F" dir "/")))]
[_ '()])
`((url . ,trimmed-source)
(rev . ,rev)
(type . "git")
;; TODO: sha256?
))))))))]))]
[sources-from-repo (if (string=? rel-path "")
(list (cons (string->symbol package-name)
(make-immutable-hash
`((0.0.0 . ,(make-immutable-hash
`((type . "path")
(path . ,src-path))))))))
(set-map names-of-overridden-packages
(lambda (name)
(cons (string->symbol name)
(make-immutable-hash
`((0.0.0 . ,(make-immutable-hash
`((type . "path")
(path . ,(path->string (build-path parent-path (string-append-immutable name "/")))))))))))))]
[sources-hash-table (make-immutable-hash (append sources-from-catalog
sources-from-repo))]
[sources (make-immutable-hash (hash-map dependency-subgraph
(lambda (name _v)
(cons (string->symbol name) (hash-ref sources-hash-table (string->symbol name))))))]
[dream-lock (make-immutable-hash
`((_generic . ,generic)
(sources . ,sources)
(_subsystem . ,(make-immutable-hash))
(dependencies . ,(make-immutable-hash
(hash-map dependency-subgraph
(lambda (name dep-list)
(cons (string->symbol name)
(make-immutable-hash `((0.0.0 . ,(map (lambda (dep-name) (list dep-name "0.0.0")) dep-list)))))))))))])
(make-parent-directory* (getenv "RACKET_OUTPUT_FILE"))
(with-output-to-file (getenv "RACKET_OUTPUT_FILE")
(lambda () (write-json dream-lock))
#:exists 'replace)))