Skip to content

hymkor/smake

Repository files navigation

SMake: Make Powered by S-expressions

SMake is a build tool similar to make on UNIX systems, but it uses S-expressions for its Makefile syntax.

Why smake?

Makefiles written for GNU Make often depend heavily on /bin/sh or other Unix-specific shell features, making it difficult to write portable build scripts that run the same on both Windows and Linux.

smake is a minimal build tool that avoids this problem by embedding all behavior in a Lisp dialect (ISLisp-like). All build logic is expressed using built-in functions, avoiding external shell commands and Unix-specific syntax.

This approach enables truly cross-platform builds without writing platform-specific conditionals, and without assuming Unix-like tools on Windows.

Example Usage

Using smake (Installed)

> cd "./examples/cc"

> smake
gcc -c main.c
gcc -c sub.c
gcc -o cc.exe main.o sub.o

> smake clean
rm "main.o"
rm "sub.o"
rm "cc.exe"

Using smake (via go run)

> cd "./examples/cc"

> go run github.com/hymkor/smake@latest
go: downloading github.com/hymkor/smake v0.5.0
gcc -c main.c
gcc -c sub.c
gcc -o cc.exe main.o sub.o

> go run github.com/hymkor/smake@latest clean
rm "main.o"
rm "sub.o"
rm "cc.exe"

examples/cc/Makefile.lsp:

;; # Equivalent Makefile for GNU Make (for reference)
;; ifeq ($(OS),Windows_NT)
;;   EXE=.exe
;; else
;;   EXE=
;; endif
;; AOUT=$(notdir $(CURDIR))$(EXE)
;; OFILES=$(subst .c,.o,$(wildcard *.c))
;; $(AOUT): $(OFILES)
;;     gcc -o $@ $(OFILES)
;; .c.o:
;;     gcc -c $<

(defun c-to-o (c) (string-append (basename c) ".o"))

(defglobal c-files (wildcard "*.c"))
(defglobal o-files (mapcar #'c-to-o c-files))
(defglobal target  (string-append (notdir (getwd)) *exe-suffix*))

(case $1
  (("clean")
   (dolist (obj o-files)
     (if (probe-file obj)
       (rm obj)))
   (if (probe-file target)
     (rm target)))

  (t
    (dolist (c-src c-files)
      (if (updatep (c-to-o c-src) c-src)
        (spawn "gcc" "-c" c-src)))
    (apply #'spawn "gcc" "-o" target o-files))
  ) ; case

; vim:set lispwords+=apply,make:

Install

Download the zipfile for your environment from Releases and unzip.

Use Go-installer

go install github.com/hymkor/smake@latest

Use scoop-installer

scoop install https://raw.githubusercontent.com/hymkor/smake/master/smake.json

or

scoop bucket add hymkor https://github.com/hymkor/scoop-bucket
scoop install smake

How to build SMake

go build

Lisp References

Base interpreter

ISLisp Documents (English)

ISLisp Documents (Japanese)

Information about ISLisp is still limited, but if you're looking for more insights, ChatGPT can be surprisingly knowledgeable and might provide helpful answers. Feel free to give it a try.

The functions available in Makefile.lsp

(updatep TARGET SOURCES...)

It returns the list of newer files in SOURCES than TARGET

(spawn "COMMAND" "ARG-1" "ARG-2" ...)

Execute the external executable directly. If it fails, the process will stop with an error.

(q "COMMAND" "ARG-1" "ARG-2" ...)

Execute the external executable directly and return its standard-output as string.

(sh "SHELL-COMMAND")

Execute the shell command by CMD.exe or /bin/sh. If it fails, the build process stops with an error.

(sh-ignore-error "SHELL-COMMAND")

Equivalent to (sh) but ignores errors.

(shell "SHELL-COMMAND")

Execute the shell command and return its standard-output as string. Equivalent to $(shell "..") of GNU Make.

(echo STRING...)

Equivalent to the UNIX echo command.

(rm FILENAME...)

Equivalent to the UNIX rm command.

(touch FILENAME...)

Equivalent to the UNIX touch command.

(dolist (KEY '(VALUE...)) COMMANDS...)

(getenv "NAME")

Return the value of the environment variable NAME. If it does not exist, return nil.

(setenv "NAME" "VALUE")

Set the environment variable "NAME" to "VALUE".

(env (("NAME" "VALUE")...) COMMANDS...)

Set the environment variables and execute COMMANDS. Then, restores them to their original values.

(wildcard "PATTERN"...)

Expand PATTERNs as a wildcard and return them as a list.

(abspath "FILEPATH")

Equivalent to $(abspath FILEPATH) of GNU Make

(dir "FILEPATH")

Equivalent to $(dir FILEPATH) of GNU Make

(notdir "FILEPATH")

Equivalent to $(notdir FILEPATH) of GNU Make

(basename "FILEPATH")

Equivalent to $(basename FILEPATH) of GNU Make

(join-path "DIR" .. "FNAME")

Make path with "DIR"... "FNAME".

(probe-file FILENAME)

If FILENAME exists, it returns t. Otherwise nil. Equivalent to -e FILENAME of Perl.

(probe-directory DIRNAME)

If DIRNAME exists and it is a directory, it returns t. Otherwise nil. Equivalent to -d FILENAME of Perl.

(chdir "DIRNAME")

Change the current working directory to "DIRNAME"

(getwd)

Returns the current working directory.

(pushd "DIRNAME" COMMANDS)

Change the current directory to "DIRNAME" and execute COMMANDS like (progn). After COMMANDS, return to the original current directory.

(cp SRC... DST)

Copy file SRC... to DST (directory or new filename)

(mv SRC... DST)

Move file SRC... to DST (directory or new filename)

(string-split SEP SEQUENCE)

(string-split #\: "a:b:c") => ("a" "b" "c")

(shellexecute "ACTION" "PATH" ["PARAM"] ["DIRECTORY"])

Call Windows-API: shellexecute

(string-fields "STRING")

Split "STRING" with white-spaces. This function is similar with strings.Fields in golang

(let), (format) and so on

These are standard functions compatible with ISLisp. See also hymkor/gmnlisp

(match REGULAR-EXPRESSION STRING)

If REGULAR-EXPRESSION matches STRING, (match) returns a list containing the entire matched part followed by the captured groups (submatches). If there is no match, it returns nil.

(if-some (VAR EXPR) THEN ELSE)

Evaluate EXPR and bind the result to VAR. If the result is not nil, execute the THEN expression. Otherwise, execute the ELSE expression. Note: only one expression is allowed for both THEN and ELSE. Also, VAR is visible in both THEN and ELSE parts.

Example:

(if-some (val (getenv "CONFIG"))
  (format t "Config is: ~A~%" val)
  (format t "No config found.~%"))

(when-some (VAR EXPR) BODY...)

Evaluate EXPR and bind the result to VAR. If the result is not nil, execute the BODY expressions (one or more). This is useful when you want to avoid writing separate let and if forms just to handle optional values.

Example:

(when-some (val (getenv "USERNAME"))
  (format t "User: ~A~%" val)
  (format t "Welcome!~%"))

(cond-let CONDITION-CLAUSES...)

Evaluates a series of condition-and-body pairs, where each condition may optionally include a variable binding. It works like a combination of cond and let. Each clause consists of either:

  • ((VAR EXPR) TEST) — bind the result of EXPR to VAR, then evaluate TEST.
  • or just (TEST) — evaluate TEST directly.

If the test is true (non-nil), the associated body expression is executed. Only one matching clause is executed. The final fallback clause may use t as a catch-all.

Example:

(cond-let
  ((x (getenv "USER")) (stringp x)) (format t "USER: ~A~%" x)
  ((y (getenv "USERNAME")) t)       (format t "USERNAME: ~A~%" y)
  (t                                (format t "No user info~%")))

This macro allows concise conditional logic with optional value binding.

(which EXECUTABLE-NAME)

The which function searches for files with a given name along the directories listed in the PATH environment variable, similar to the Unix which command.

(which "go1.20.14")

This returns a list of file paths where an executable named go1.20.14 was found. On Windows, it automatically checks for file extensions such as .exe, .cmd, and .bat.

Example (on Windows):

> smake -e "(format (standard-output) \"~S~%\" (which \"nyagos\"))"
("C:\\Users\\hymko\\Share\\bin\\nyagos.exe"
 "C:\\Users\\hymko\\Share\\bin\\nyagos"
 "C:\\Users\\hymko\\Share\\amd64\\nyagos.exe"
 "C:\\Users\\hymko\\go\\bin\\nyagos.exe")

(executable-not-found-p CONDITION)

If CONDITION is an error raised by (spawn ...) due to the specified command not being found, this function returns t. Otherwise, it returns nil. It is typically used within with-handler to detect missing executables when invoking external commands.

(exit-error-p CONDITION)

If CONDITION is an error raised by (spawn ...) because the command exited with a non-zero status code, this function returns t. Otherwise, it returns nil. Use it in with-handler to distinguish execution failures caused by external commands.

(exit-code CONDITION)

Returns the exit code of a command that was invoked by (spawn ...), if CONDITION represents a non-zero exit status. If CONDITION is not such an error (i.e., (exit-error-p CONDITION) returns nil), this function raises a <domain-error>.

(ansi-to-utf8 STRING)

On Windows, ansi-to-utf8 converts the given string from the current ANSI code page to UTF-8. On other platforms, it returns the string unchanged.

The built-in variables

*windows*

Evaluates to t when the environment variable %OS% is "Windows_NT" (i.e., on Windows).

*dev-null*

Evaluates to "NUL" on Windows, and to "/dev/null" on other systems.

*exe-suffix*

The file extension used for executables on the current operating system (e.g., ".exe" on Windows; empty string on Unix).

*argv* (formerly *args*)

The command-line arguments passed to the program.

$0, $1, $2, .. $9

Equivalent to (and (<= N (length *args*)) (elt (cons "path-to-smake" *args*) N)) where "path-to-smake" is the full path to the SMake executable.

*path-separator*

OS-specific path separator (Windows "\" , UNIX "/" )

*path-list-separator*

OS-specfic path list separator (Windows ";", UNIX ":")

*executable-name*

The path of smake's executable

*discard*

*discard* is a global output stream that silently ignores all writes.

License

MIT License

Author

HAYAMA_Kaoru (hymkor)


About

SMake: Make Powered by S-expressions

Topics

Resources

License

Stars

Watchers

Forks