Building programs and using packages
How to turn a Goose program into an executable, how to use libraries that are not part of the compiler, and how to make one. The design behind it, and the compiler's side of it, is in design/packages.md.
1. Running and building
goose foo.goose (or goose run foo.goose)
compiles the program into the compiler's own process and runs it there:
no C compiler, no build step, the arguments after -- go to
the program. goose build foo.goose makes an executable,
foo or foo.exe beside the source:
goose build foo.goose && ./foo
goose build -o out/foo foo.goose # the executable elsewhere
goose build --debug foo.goose # GS_DEBUG checks, debug information, no C optimization
goose build --standalone foo.goose # the runtime inside the one C file
goose build --dry-run foo.goose # print the commands instead of running them
goose build -v foo.goose # print them as they runbuild finds a C compiler on the machine
(goose toolchain says which: MSVC or clang on Windows, cc,
gcc or clang elsewhere; --cc clang, GOOSE_CC,
or cc = "clang"; in ~/.goose/config choose),
compiles the runtime once per compiler into ~/.goose, and
links the program with the native parts of every package it uses.
goose check foo.goose only typechecks. A build defaults to
-O2, a run to -O1.
Everything the compiler makes on the way goes under
~/.goose (GOOSE_HOME moves it): fetched
packages, their builds, the runtime objects and each program's C.
goose build --dry-run shows the commands with their paths,
for a Makefile or CMake project that wants to drive the C compiler
itself.
Work that takes a while (fetching a package, building a package's C,
compiling the runtime) prints one line as it starts. On a terminal a
live line below it shows what is running, for how long, and the last
line the tool printed, and is erased when the work is done. When the
output goes anywhere else (a file, a pipe, a CI log, an agent running
the command) there is no live line, only those one-line notes.
GOOSE_PROGRESS=plain or GOOSE_PROGRESS=live
overrides the choice; -v and --dry-run print
plainly.
2. Using a package
A package is a library in a directory of its own: a module
named after it, name.goose, further modules beside it, and
possibly C. The packages the compiler knows by name are the
blessed ones (src/blessed.txt): a program imports
them like any module,
import gfx;
import physics;and the compiler fetches the package from its repository at the
pinned tag the first time, into ~/.goose/pkg, builds its C
with cmake and the C compiler, and keeps both. A compiler configured
with -DGOOSE_BUILTIN_LIBS=all has the blessed packages
compiled in and runs such a program with nothing else installed;
goose toolchain lists what is built in.
Any other package, or another version of a blessed one, is declared in the program's root file (the one named on the command line), with its source:
import mylib from "joe/mylib@v2"; // GitHub, at tag v2
import mylib from "github.com/joe/mylib@3f2c1a9"; // any host, at a commit
import mylib from "https://git.example.com/x/mylib.git@main"; // any git URL, at a branch
import mylib from "../mylib"; // a directory, used in placeimport name from "source"; declares the package
name and imports its module name.goose; its
other modules are import name.other;. A git source needs a
ref after @: a tag or a commit is a pin, and a branch is
one too until goose fetch --update program.goose fetches it
again. A path is relative to the file that writes it, and nothing is
copied: edit the library and run the program.
Only the root file declares packages. Every other
file, the library's own included, imports by name alone. A library that
needs another package writes the same
import other from "…"; line in its own files, and the
compiler reads it as a requirement: if the root does not
declare other, the error says exactly what line to add to
the root. The root declaring the same package from a different ref gets
a warning; from a different source, an error. So a program's
dependencies are flat, pinned, and in one place, and one version of each
package exists in a program.
goose fetch program.goose fetches everything the root
declares, for an offline session later. The tools involved are reported
by name when they are missing: git to fetch,
cmake for a package built with CMake, a C compiler for any
C at all. A program with no C and no such packages needs none of
them.
3. A program with C of its own
C that belongs to the program goes in a sidecar beside the root file,
foo.build beside foo.goose:
// foo.build
c_sources = ["impl.c", "vendor/stb_impl.c"]; // compiled with the program's build
c_include = ["vendor"];
c_defines = ["IMPL_FAST=1"];
c_libs = ["m"]; // system libraries, by name
c_libs_windows = ["user32"]; // one platform's part
output = "foo"; // the executable's name, if not the file'sThe file looks like Goose (// comments,
key = value;, strings, true and
false, [lists]), and every c_*
key takes a _windows, _linux or
_macos suffix for a platform-only part.
goose build compiles and links that C;
goose run builds it into a library the in-process run
loads. Either way an extern fn in the program reaches
it:
extern fn impl_sum(a: i32, b: i32) -> i32;
extern fn impl_count(s: const u8[:], c: u8) -> i64; // a slice arrives as { data, len }A single-header library is one impl.c that defines its
implementation macro and includes the header. A header spliced into the
generated C instead, --include shim.h on the command line,
still works for small shims written against the generated types (spec
§7.10).
4. Making a package
A package is a git repository (or any directory) laid out like this:
mylib/
mylib.goose the entry: `namespace mylib;`, its declarations
mylib.build the sidecar, when there is C to build (optional)
extra.goose another module: `import mylib.extra;`
src/, include/ the C, if any
examples/, test/ programs of its own, each a root file
README.md
Inside the package, import helper; finds
helper.goose under the package's root,
import .helper; finds it relative to the importing file,
and import std; reaches the standard library. By convention
a package's files declare its name as their namespace.
A library that needs another package writes the requirement in its
entry,
import vec_extras from "github.com/x/vec_extras@v2";, which
the compiler checks against the program's root as described above. Its
own examples and tests are root files, so there the line is honored and
the examples run on their own.
The native part
The sidecar names the C, one of two ways. Plain sources, which the compiler compiles with the includes, defines, flags and libraries given:
// mylib.build
c_sources = ["src/mylib.c"];
c_include = ["include"];
c_libs_linux = ["m"];Or a CMake project at the package root, by the name of its static library target:
// mylib.build
cmake_target = "mylib";
cmake_options = ["MYLIB_SIMD=ON"];The compiler then configures and builds a small wrapper project
around the package's own CMakeLists and takes the target's whole link
line, so a package can be a thin Goose layer over an existing C library
with no CMake written for Goose's sake. Submodules the package needs are
cloned with it. Either way the part is built once per C compiler under
~/.goose/build, and a program links it statically.
The functions the package's extern fns name must be
defined by that C (or by the C library); the runtime's own
gs_os_* and friends belong to the standard library. Every
.goose file under the package root counts as the package's,
except in a directory below the root holding a sidecar of its own (an
example program with its own C, say) and in the directories the sidecar
lists as not the package's:
// mylib.build
exclude = ["tools", "third_party/demo"];Most values an extern fn passes have layouts C can
declare for itself, a slice as { data, len }, say (spec
§7.10). A builder, u8[>..]&, is the exception: C
appends to it through the runtime, which goose.h gives C
compiled on its own. The compiler puts that header on the include path
of every native part, the program's own included, CMake targets too, and
builds them all again when it changes:
#include "goose.h"
void mylib_name(gs_rref out, int64_t id) { /* extern fn mylib_name(out: u8[>..]&, id: i64); */
gs_bld_append(out, "item", 4);
}A package's C is self-contained: two packages that must share C share
a repository, as gfx, audio and
ui do. A package whose functions keep one state for the
process, or that may only be called from the main thread, says so in its
sidecar, main_thread_only = true;, and a
thread_fn calling it is then a compile error.
Files a package needs at run time
embed_file("path") reads a file when the program is
compiled, relative to the file with the call, and gives its bytes as a
const u8[:] into static data: a font, a table, a shader
source. That is how a package ships data without a deployment story.
Files read at run time belong to the program
(resource_dir(), docs/stdlib.md).
Publishing
Push the repository, tag a version, and tell users the line to write:
import mylib from "github.com/you/mylib@v1";There is no registry to register with. A tag can move and a branch always does, so users who need a build to be reproducible pin a commit. Code from a fetched package runs on the user's machine when its C is built, as with any build script; that is the trust a pin expresses.
5. The cache
~/.goose/
config cc = "clang";
pkg/<host>/<owner>/<repo>/<ref>/ fetched packages, submodules included
build/pkg/.../<toolchain>/ their native parts, per C compiler
build/runtime/<hash>/... the runtime object, per C compiler and configuration
build/prog/<name>-<hash>/ each program's C, objects and logs
toolchain/ what finding the C compiler took (vcvars on Windows)
Deleting it loses nothing that a build does not make again.
Goose