SMake is a build tool similar to make
on UNIX systems, but it uses S-expressions for its Makefile syntax.
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.
> 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"
> 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"
;; # 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:
Download the zipfile for your environment from Releases and unzip.
go install github.com/hymkor/smake@latest
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
go build
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.
It returns the list of newer files in SOURCES than TARGET
Execute the external executable directly. If it fails, the process will stop with an error.
Execute the external executable directly and return its standard-output as string.
Execute the shell command by CMD.exe or /bin/sh. If it fails, the build process stops with an error.
Equivalent to (sh)
but ignores errors.
Execute the shell command and return its standard-output as string.
Equivalent to $(shell "..")
of GNU Make.
Equivalent to the UNIX echo command.
Equivalent to the UNIX rm command.
Equivalent to the UNIX touch command.
Return the value of the environment variable NAME. If it does not exist, return nil.
Set the environment variable "NAME"
to "VALUE"
.
Set the environment variables and execute COMMANDS. Then, restores them to their original values.
Expand PATTERNs as a wildcard and return them as a list.
Equivalent to $(abspath FILEPATH)
of GNU Make
Equivalent to $(dir FILEPATH)
of GNU Make
Equivalent to $(notdir FILEPATH)
of GNU Make
Equivalent to $(basename FILEPATH)
of GNU Make
Make path with "DIR"... "FNAME".
If FILENAME exists, it returns t. Otherwise nil.
Equivalent to -e FILENAME
of Perl.
If DIRNAME exists and it is a directory, it returns t. Otherwise nil.
Equivalent to -d FILENAME
of Perl.
Change the current working directory to "DIRNAME"
Returns the current working directory.
Change the current directory to "DIRNAME" and execute COMMANDS like (progn). After COMMANDS, return to the original current directory.
Copy file SRC... to DST (directory or new filename)
Move file SRC... to DST (directory or new filename)
(string-split #\: "a:b:c")
=> ("a" "b" "c")
Call Windows-API: shellexecute
Split "STRING" with white-spaces. This function is similar with strings.Fields in golang
These are standard functions compatible with ISLisp. See also hymkor/gmnlisp
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
.
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.~%"))
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!~%"))
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 ofEXPR
toVAR
, then evaluateTEST
.- 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.
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
.
> 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")
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.
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.
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>
.
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.
Evaluates to t
when the environment variable %OS%
is "Windows_NT"
(i.e., on Windows).
Evaluates to "NUL"
on Windows, and to "/dev/null"
on other systems.
The file extension used for executables on the current operating system (e.g., ".exe"
on Windows; empty string on Unix).
The command-line arguments passed to the program.
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.
OS-specific path separator (Windows "\"
, UNIX "/"
)
OS-specfic path list separator (Windows ";"
, UNIX ":"
)
The path of smake's executable
*discard*
is a global output stream that silently ignores all writes.
MIT License
HAYAMA_Kaoru (hymkor)