2021-12-15 17:31:47 +01:00
;; magrathea helps you build planets
;;
;; it is a tiny tool designed to ease workflows in monorepos that are
;; modeled after the tvl depot.
;;
;; users familiar with workflows from other, larger monorepos may be
;; used to having a build tool that can work in any tree location.
;; magrathea enables this, but with nix-y monorepos.
( import ( chicken base )
2021-12-17 19:17:28 +01:00
( chicken format )
2021-12-15 17:31:47 +01:00
( chicken irregex )
2021-12-17 19:17:28 +01:00
( chicken port )
2022-02-13 19:30:29 +01:00
( chicken file )
( chicken file posix )
2021-12-15 17:31:47 +01:00
( chicken process )
( chicken process-context )
( chicken string )
2021-12-17 23:08:40 +01:00
( matchable )
( only ( chicken io ) read-string ) )
2021-12-15 17:31:47 +01:00
( define usage # <<USAGE
usage: mg <command> [ <target> ]
2022-11-25 18:47:01 +01:00
mg run [ <target> ] [ -- <arguments> ]
2021-12-15 17:31:47 +01:00
target:
a target specification with meaning inside of the repository . can
be absolute ( starting with // ) or relative to the current directory
( as long as said directory is inside of the repo ) . if no target is
specified, the current directory 's physical target is built .
for example:
//tools/magrathea - absolute physical target
//foo/bar:baz - absolute virtual target
magrathea - relative physical target
:baz - relative virtual target
commands:
build - build a target
shell - enter a shell with the target 's build dependencies
2021-12-17 23:42:05 +01:00
path - print source folder for the target
2022-06-01 12:27:59 +02:00
repl - start a nix repl in the repository root
2022-02-13 19:30:29 +01:00
run - build a target and execute its output
2021-12-15 17:31:47 +01:00
file all feedback on b . tvl . fyi
USAGE
)
2021-12-17 19:17:28 +01:00
;; parse target definitions. trailing slashes on physical targets are
;; allowed for shell autocompletion.
;;
;; component ::= any string without "/" or ":"
;;
;; physical-target ::= <component>
;; | <component> "/"
;; | <component> "/" <physical-target>
;;
;; virtual-target ::= ":" <component>
;;
;; relative-target ::= <physical-target>
;; | <virtual-target>
;; | <physical-target> <virtual-target>
;;
;; root-anchor ::= "//"
;;
;; target ::= <relative-target> | <root-anchor> <relative-target>
;; read a path component until it looks like something else is coming
( define ( read-component first port )
( let ( ( keep-reading?
( lambda ( ) ( not ( or ( eq? #\/ ( peek-char port ) )
( eq? #\: ( peek-char port ) )
( eof-object? ( peek-char port ) ) ) ) ) ) )
( let reader ( ( acc ( list first ) )
( condition ( keep-reading? ) ) )
( if condition ( reader ( cons ( read-char port ) acc ) ( keep-reading? ) )
( list->string ( reverse acc ) ) ) ) ) )
;; read something that started with a slash. what will it be?
( define ( read-slash port )
( if ( eq? #\/ ( peek-char port ) )
( begin ( read-char port )
'root-anchor )
'path-separator ) )
;; read any target token and leave port sitting at the next one
( define ( read-token port )
( match ( read-char port )
[ #\/ ( read-slash port ) ]
[ #\: 'virtual-separator ]
[ other ( read-component other port ) ] ) )
;; read a target into a list of target tokens
( define ( read-target target-str )
( call-with-input-string
target-str
( lambda ( port )
( let reader ( ( acc ' ( ) ) )
( if ( eof-object? ( peek-char port ) )
( reverse acc )
( reader ( cons ( read-token port ) acc ) ) ) ) ) ) )
( define-record target absolute components virtual )
2021-12-17 23:08:40 +01:00
( define ( empty-target ) ( make-target #f ' ( ) #f ) )
2021-12-17 19:17:28 +01:00
( define-record-printer ( target t out )
2021-12-17 23:08:40 +01:00
( fprintf out ( conc ( if ( target-absolute t ) "//" "" )
2021-12-17 19:17:28 +01:00
( string-intersperse ( target-components t ) "/" )
( if ( target-virtual t ) ":" "" )
2021-12-17 23:08:40 +01:00
( or ( target-virtual t ) "" ) ) ) )
2021-12-17 19:17:28 +01:00
;; parse and validate a list of target tokens
( define parse-tokens
2021-12-17 23:08:40 +01:00
( lambda ( tokens # !optional ( mode 'root ) ( acc ( empty-target ) ) )
2021-12-17 19:17:28 +01:00
( match ( cons mode tokens )
;; absolute target
[ ( 'root . ( 'root-anchor . rest ) )
( begin ( target-absolute-set! acc #t )
( parse-tokens rest 'root acc ) ) ]
;; relative target minus potential garbage
[ ( 'root . ( not ( 'path-separator . _ ) ) )
( parse-tokens tokens 'normal acc ) ]
;; virtual target
[ ( 'normal . ( 'virtual-separator . rest ) )
( parse-tokens rest 'virtual acc ) ]
[ ( 'virtual . ( ( ? string? v ) ) )
( begin
( target-virtual-set! acc v )
acc ) ]
;; chomp through all components and separators
[ ( 'normal . ( 'path-separator . rest ) ) ( parse-tokens rest 'normal acc ) ]
[ ( 'normal . ( ( ? string? component ) . rest ) )
2021-12-17 23:08:40 +01:00
( begin ( target-components-set!
acc ( append ( target-components acc ) ( list component ) ) )
2021-12-17 19:17:28 +01:00
( parse-tokens rest 'normal acc ) ) ]
;; nothing more to parse and not in a weird state, all done, yay!
[ ( 'normal . ( ) ) acc ]
;; oh no, we ran out of input too early :(
[ ( _ . ( ) ) ` ( error . , ( format "unexpected end of input while parsing ~s target" mode ) ) ]
;; something else was invalid :(
[ _ ` ( error . , ( format "unexpected ~s while parsing ~s target" ( car tokens ) mode ) ) ] ) ) )
( define ( parse-target target )
( parse-tokens ( read-target target ) ) )
2021-12-17 23:08:40 +01:00
;; turn relative targets into absolute targets based on the current
;; directory
( define ( normalise-target t )
( when ( not ( target-absolute t ) )
( target-components-set! t ( append ( relative-repo-path )
( target-components t ) ) )
( target-absolute-set! t #t ) )
t )
;; nix doesn't care about the distinction between physical and virtual
;; targets, normalise it away
( define ( normalised-components t )
( if ( target-virtual t )
( append ( target-components t ) ( list ( target-virtual t ) ) )
( target-components t ) ) )
2021-12-15 17:31:47 +01:00
;; return the current repository root as a string
( define mg--repository-root #f )
( define ( repository-root )
( or mg--repository-root
( begin
( set! mg--repository-root
2021-12-17 23:52:22 +01:00
( or ( get-environment-variable "MG_ROOT" )
2023-06-22 13:40:09 +02:00
( call-with-input-pipe "git rev-parse --show-toplevel"
( lambda ( p ) ( read-chomping p ) ) ) ) )
2021-12-15 17:31:47 +01:00
mg--repository-root ) ) )
;; determine the current path relative to the root of the repository
;; and return it as a list of path components.
( define ( relative-repo-path )
( string-split
( substring ( current-directory ) ( string-length ( repository-root ) ) ) "/" ) )
;; escape a string for interpolation in nix code
( define ( nix-escape str )
( string-translate* str ' ( ( "\"" . "\\\"" )
( "${" . "\\${" ) ) ) )
;; create a nix expression to build the attribute at the specified
;; components
;;
2021-12-17 23:08:40 +01:00
;; an empty target will build the current folder instead.
;;
2021-12-15 17:31:47 +01:00
;; this uses builtins.getAttr explicitly to avoid problems with
;; escaping.
2021-12-17 23:08:40 +01:00
( define ( nix-expr-for target )
( let nest ( ( parts ( normalised-components ( normalise-target target ) ) )
( acc ( conc "(import " ( repository-root ) " {})" ) ) )
2021-12-15 17:31:47 +01:00
( match parts
[ ( ) ( conc "with builtins; " acc ) ]
2021-12-17 23:08:40 +01:00
[ _ ( nest ( cdr parts )
( conc "(getAttr \""
( nix-escape ( car parts ) )
"\" " acc ")" ) ) ] ) ) )
;; exit and complain at the user if something went wrong
2021-12-17 23:42:05 +01:00
( define ( mg-error message )
( format ( current-error-port ) "[mg] error: ~A~%" message )
( exit 1 ) )
2021-12-17 23:08:40 +01:00
( define ( guarantee-success value )
( match value
2021-12-17 23:42:05 +01:00
[ ( 'error . message ) ( mg-error message ) ]
2021-12-17 23:08:40 +01:00
[ _ value ] ) )
2022-04-27 16:10:52 +02:00
( define-record build-args target passthru unknown )
( define ( execute-build args )
( let ( ( expr ( nix-expr-for ( build-args-target args ) ) ) )
( fprintf ( current-error-port ) "[mg] building target ~A~%" ( build-args-target args ) )
( process-execute "nix-build" ( append ( list "-E" expr "--show-trace" )
( or ( build-args-passthru args ) ' ( ) ) ) ) ) )
;; split the arguments used for builds into target/unknown args/nix
;; args, where the latter occur after '--'
( define ( parse-build-args acc args )
2021-12-15 17:31:47 +01:00
( match args
2022-04-27 16:10:52 +02:00
;; no arguments remaining, return accumulator as is
[ ( ) acc ]
;; next argument is '--' separator, split off passthru and
;; return
[ ( "--" . passthru )
( begin
( build-args-passthru-set! acc passthru )
acc ) ]
[ ( arg . rest )
;; set target if not already known (and if the first
;; argument does not look like an accidental unknown
;; parameter)
( if ( and ( not ( build-args-target acc ) )
( not ( substring=? "-" arg ) ) )
( begin
( build-args-target-set! acc ( guarantee-success ( parse-target arg ) ) )
( parse-build-args acc rest ) )
;; otherwise, collect unknown arguments
( begin
( build-args-unknown-set! acc ( append ( or ( build-args-unknown acc ) ' ( ) )
( list arg ) ) )
( parse-build-args acc rest ) ) ) ] ) )
;; parse the passed build args, applying sanity checks and defaulting
;; the target if necessary, then execute the build
( define ( build args )
( let ( ( parsed ( parse-build-args ( make-build-args #f #f #f ) args ) ) )
;; fail if there are unknown arguments present
( when ( build-args-unknown parsed )
( let ( ( unknown ( string-intersperse ( build-args-unknown parsed ) ) ) )
( mg-error ( sprintf " unknown arguments: ~a
if you meant to pass these arguments to nix, please separate them with
'-- ' like so:
mg build ~a -- ~a "
unknown
( or ( build-args-target parsed ) "" )
unknown ) ) ) )
2021-12-17 23:08:40 +01:00
2022-04-27 16:10:52 +02:00
;; default the target to the current folder's main target
( unless ( build-args-target parsed )
( build-args-target-set! parsed ( empty-target ) ) )
2021-12-17 23:08:40 +01:00
2022-04-27 16:10:52 +02:00
( execute-build parsed ) ) )
2021-12-15 17:31:47 +01:00
2021-12-17 23:08:40 +01:00
( define ( execute-shell t )
( let ( ( expr ( nix-expr-for t ) )
2021-12-15 17:31:47 +01:00
( user-shell ( or ( get-environment-variable "SHELL" ) "bash" ) ) )
2022-02-13 17:44:43 +01:00
( fprintf ( current-error-port ) "[mg] entering shell for ~A~%" t )
2021-12-15 17:31:47 +01:00
( process-execute "nix-shell"
( list "-E" expr "--command" user-shell ) ) ) )
( define ( shell args )
( match args
2021-12-17 23:08:40 +01:00
[ ( ) ( execute-shell ( empty-target ) ) ]
[ ( arg ) ( execute-shell
( guarantee-success ( parse-target arg ) ) ) ]
2021-12-15 17:31:47 +01:00
[ other ( print "not yet implemented" ) ] ) )
2022-06-01 12:27:59 +02:00
( define ( repl args )
( process-execute "nix" ( append ( list "repl" "--show-trace" ( repository-root ) ) args ) ) )
2023-06-22 13:40:09 +02:00
( define ( read-chomping pipe )
( let ( ( s ( read-string #f pipe ) ) )
( if ( eq? s # !eof ) "" ( string-chomp s ) ) ) )
2022-02-13 19:30:29 +01:00
( define ( execute-run t # !optional cmd-args )
( fprintf ( current-error-port ) "[mg] building target ~A~%" t )
( let* ( ( expr ( nix-expr-for t ) )
2022-11-10 12:44:13 +01:00
( out
( receive ( pipe _ pid )
;; TODO(sterni): temporary gc root
( process "nix-build" ( list "-E" expr "--no-out-link" ) )
2023-06-22 13:40:09 +02:00
( let ( ( stdout ( read-chomping pipe ) ) )
2022-11-10 12:44:13 +01:00
( receive ( _ _ status )
( process-wait pid )
( when ( not ( eq? status 0 ) )
2023-06-22 13:40:09 +02:00
( mg-error ( format "Couldn't build target ~A" t ) ) )
2022-11-10 12:44:13 +01:00
stdout ) ) ) ) )
2022-02-13 19:30:29 +01:00
( fprintf ( current-error-port ) "[mg] running target ~A~%" t )
( process-execute
;; If the output is a file, we assume it's an executable à la writeExecline,
;; otherwise we look in the bin subdirectory and pick the only executable.
;; Handling multiple executables is not possible at the moment, the choice
;; could be made via a command line flag in the future.
( if ( regular-file? out )
out
( let* ( ( dir-path ( string-append out "/bin" ) )
( dir-contents ( if ( directory-exists? dir-path )
( directory dir-path #f )
' ( ) ) ) )
( case ( length dir-contents )
( ( 0 ) ( mg-error "no executables in build output" )
( exit 1 ) )
( ( 1 ) ( string-append dir-path "/" ( car dir-contents ) ) )
( else ( mg-error "more than one executable in build output" )
( exit 1 ) ) ) ) )
cmd-args ) ) )
( define ( run args )
( match args
[ ( ) ( execute-run ( empty-target ) ) ]
2022-11-25 18:47:01 +01:00
[ ( "--" . rest ) ( execute-run ( empty-target ) rest ) ]
2023-06-22 13:05:20 +02:00
[ ( target ) ( execute-run ( guarantee-success ( parse-target target ) ) ) ]
2022-11-25 18:47:01 +01:00
[ ( target . ( "--" . rest ) ) ( execute-run ( guarantee-success ( parse-target target ) ) rest ) ]
2022-02-13 19:30:29 +01:00
;; TODO(sterni): flag for selecting binary name
2022-11-25 18:47:01 +01:00
[ _ ( mg-error "usage: mg run [<target>] [-- <arguments>] (hint: use \"--\" to separate the `mg run [<target>]` invocation from the arguments you're passing to the built executable)" ) ] ) )
2022-02-13 19:30:29 +01:00
2021-12-17 23:42:05 +01:00
( define ( path args )
( match args
[ ( arg )
2021-12-18 20:27:37 +01:00
( print ( apply string-append
( intersperse
( cons ( repository-root )
( target-components
( normalise-target
( guarantee-success ( parse-target arg ) ) ) ) )
"/" ) ) ) ]
2021-12-17 23:42:05 +01:00
[ ( ) ( mg-error "path command needs a target" ) ]
[ other ( mg-error ( format "unknown arguments: ~a" other ) ) ] ) )
2021-12-15 17:31:47 +01:00
( define ( main args )
( match args
[ ( ) ( print usage ) ]
2021-12-17 23:08:40 +01:00
[ ( "build" . _ ) ( build ( cdr args ) ) ]
[ ( "shell" . _ ) ( shell ( cdr args ) ) ]
2021-12-17 23:42:05 +01:00
[ ( "path" . _ ) ( path ( cdr args ) ) ]
2022-06-01 12:27:59 +02:00
[ ( "repl" . _ ) ( repl ( cdr args ) ) ]
2022-02-13 19:30:29 +01:00
[ ( "run" . _ ) ( run ( cdr args ) ) ]
2021-12-15 17:31:47 +01:00
[ other ( begin ( print "unknown command: mg " args )
( print usage ) ) ] ) )
( main ( command-line-arguments ) )