What's the optimal experience for Go?
Assume we can do in-bramble incremental builds? With that I could do this:
def build():
return go_build(files["./"])
def mod_tidy():
return go_mod_tidy(files["./"])
and then you just bramble build :build
and bramble run :mod_tidy
.
This does create a bit of a duality though. What if someone wants to use the Go command in a way that's not available/supported.
You could just run go build
and then you'd need to build everything again from scratch.
You could just run bramble run :go_build go build
but then the go command is going to build stuff the way it likes to build things and it will ignore the artifacts available within that context. The cache would be cleared on every run. You also can't fix this in software because even a go
command that is aware of the build cache won't be able to write new stuff to the store.
So if this is how it all plays out it seems like people would have an unsatisfying experience and try and work towards compromises, so what would be ideal?
Ideal is maybe that we provide a Go command that can leverage bramble while still being the interface for the build. We basically provide a go
command and that go
command runs bramble in order to work. We could do this with bramble run
but then it would need the ability to write to the store, could provide an api for that.
So basically we want go
commands that are an alias for bramble run something
and then the go
command is really a wrapper that is interacting with bramble how we want.
Ok, so key takeaway is: allow a process in the bramble run
sandbox to write/build to the store via a socket (or whatever) to allows more dynamic use-cases.
If we start with a Go project that has a handful of dependencies. Typically you would go mod tidy
, but go mod tidy
downloads many dependencies before writing the full resolution state to go.sum
.
Ok, so what if we:
- Scan project
- Take existing go.sum and download any of those dependencies with dynamic derivations.
- Run go mod tidy
- Return more dynamic derivation derivations with that data to redownload again.
Not sure how to get around the double download problem. Even if we could get the download logic into bramble we would still need to
How would we handle basic go building?
Let's write out all the pieces and how they would work. Core assumptions:
- The project exists and has a working go.mod and go.sum file.
- Builds must be handled incrementally with derivations, no build cache on disk in a derivation that is then wiped.
First, let's download dependencies for go.mod.
If we check out vgo2nix we can see in the example deps file that it's pulling dependencies from git. Going to assume that wherever those are downloaded, when they're loaded into the build they're passed as a list to a derivation. The derivation then either loads them into GOPATH or the module package store and conducts the build. Since we're providing the functional guarantees of modules, presumably it will be ok to put modules in GOPATH and build from there. We do run into issues with case-insensitive filesystems that are solved with the module file store.
Vgo2nix also pulls the full git repo to get the sha256, we can prob skip that step. Also worth noting that it may seem tempting to want to use the GOPROXY cache instead of git, but our build cache should provide an equivalent performance improvement.
To get more detailed:
We have a derivation like this:
def foo():
build_go_mod(
go_version="1.13",
module_config=files(["go.mod", "go.sum"]),
project_files=files(["./**/*.go"]),
bramble_mod_file=file("./go_mod.bramble"),
)
This would pull all the go files into the build along with go.mod and go.sum. We also pass a special file for the bramble_mod_file
. This is the generated file that we're going to output.
To generate the bramble_mod_file
we construct a derivation within build_go_mod
that might look like this:
def build_go_mod(module_config=[], project_files=[], bramble_mod_file=None):
# Uses the network, generates the file and then references the file instead
# of using the network in future situations.
drv = special_generate_built_in(
sources=module_config,
generated_file=bramble_mod_file,
function="dependencies")
This is all magic at the moment, but the idea is that this derivation uses the network to calculate the dependencies whenever go.mod and go.sum change. From within that derivation we execute similar logic to vgo2nix and create a file that is written to disk at the generated filepath. The value of that file is then loaded dynamically into the build graph.
Once that is set up, then we need to build the binary. Need more detail here later, but the idea is that we use the output of go build -n
to create a derivation that outputs deriavations. Each build step in go build -a -n
is turned into a derivation.
So we have:
- All source files for this project
- All source files of all dependencies
When we compile a lib, we want to just look at the lib files and produce an .a file. So on a basic level, we need something that outputs the build graph. When we create derivations that reference the output of the derivation that downloaded the source.
A simple module build section for a specific module looks like this:
#
# github.com/pierrec/lz4/v3
#
mkdir -p $WORK/b305/
cat >$WORK/b305/go_asm.h << 'EOF' # internal
EOF
cd /home/maxm/go/pkg/mod/github.com/pierrec/lz4/v3@v3.3.2
/home/maxm/bramble/bramble_store_padding/bramble_/540ihg6hqyyzysnmxbs1smzb4l6s2dz5-go-1.16.9/share/go/pkg/tool/linux_amd64/asm -p github.com/pierrec/lz4/v3 -trimpath "$WORK/b305=>" -I $WORK/b305/ -I /home/maxm/bramble/bramble_store_padding/bramble_/540ihg6hqyyzysnmxbs1smzb4l6s2dz5-go-1.16.9/share/go/pkg/include -D GOOS_linux -D GOARCH_amd64 -gensymabis -o $WORK/b305/symabis ./decode_amd64.s
cat >$WORK/b305/importcfg << 'EOF' # internal
# import config
packagefile encoding/binary=$WORK/b058/_pkg_.a
packagefile errors=$WORK/b004/_pkg_.a
packagefile fmt=$WORK/b020/_pkg_.a
packagefile github.com/pierrec/lz4/v3/internal/xxh32=$WORK/b306/_pkg_.a
packagefile io=$WORK/b029/_pkg_.a
packagefile io/ioutil=$WORK/b044/_pkg_.a
packagefile math/bits=$WORK/b024/_pkg_.a
packagefile os=$WORK/b030/_pkg_.a
packagefile runtime=$WORK/b008/_pkg_.a
packagefile runtime/debug=$WORK/b160/_pkg_.a
packagefile strings=$WORK/b041/_pkg_.a
packagefile sync=$WORK/b014/_pkg_.a
EOF
cd /home/maxm/go/src/github.com/maxmcd/bramble
/home/maxm/bramble/bramble_store_padding/bramble_/540ihg6hqyyzysnmxbs1smzb4l6s2dz5-go-1.16.9/share/go/pkg/tool/linux_amd64/compile -o $WORK/b305/_pkg_.a -trimpath "$WORK/b305=>" -p github.com/pierrec/lz4/v3 -lang=go1.12 -buildid 5tJja6MFggLI9zzG4-z5/5tJja6MFggLI9zzG4-z5 -goversion go1.16.9 -symabis $WORK/b305/symabis -D "" -importcfg $WORK/b305/importcfg -pack -asmhdr $WORK/b305/go_asm.h -c=4 /home/maxm/go/pkg/mod/github.com/pierrec/lz4/v3@v3.3.2/block.go /home/maxm/go/pkg/mod/github.com/pierrec/lz4/v3@v3.3.2/debug_stub.go /home/maxm/go/pkg/mod/github.com/pierrec/lz4/v3@v3.3.2/decode_amd64.go /home/maxm/go/pkg/mod/github.com/pierrec/lz4/v3@v3.3.2/errors.go /home/maxm/go/pkg/mod/github.com/pierrec/lz4/v3@v3.3.2/lz4.go /home/maxm/go/pkg/mod/github.com/pierrec/lz4/v3@v3.3.2/lz4_go1.10.go /home/maxm/go/pkg/mod/github.com/pierrec/lz4/v3@v3.3.2/reader.go /home/maxm/go/pkg/mod/github.com/pierrec/lz4/v3@v3.3.2/writer.go
cd /home/maxm/go/pkg/mod/github.com/pierrec/lz4/v3@v3.3.2
/home/maxm/bramble/bramble_store_padding/bramble_/540ihg6hqyyzysnmxbs1smzb4l6s2dz5-go-1.16.9/share/go/pkg/tool/linux_amd64/asm -p github.com/pierrec/lz4/v3 -trimpath "$WORK/b305=>" -I $WORK/b305/ -I /home/maxm/bramble/bramble_store_padding/bramble_/540ihg6hqyyzysnmxbs1smzb4l6s2dz5-go-1.16.9/share/go/pkg/include -D GOOS_linux -D GOARCH_amd64 -o $WORK/b305/decode_amd64.o ./decode_amd64.s
/home/maxm/bramble/bramble_store_padding/bramble_/540ihg6hqyyzysnmxbs1smzb4l6s2dz5-go-1.16.9/share/go/pkg/tool/linux_amd64/pack r $WORK/b305/_pkg_.a $WORK/b305/decode_amd64.o # internal
/home/maxm/bramble/bramble_store_padding/bramble_/540ihg6hqyyzysnmxbs1smzb4l6s2dz5-go-1.16.9/share/go/pkg/tool/linux_amd64/buildid -w $WORK/b305/_pkg_.a # internal
A few notes:
cd /home/maxm/go/src/github.com/maxmcd/bramble
is very confusing. This means module compilation state depends on the source files of the end-module being compiled. This cd is passed to thecompile
command with the-D ""
flag. This means:-D path: set relative path for local imports
(fromgo tool compile
). So maybe this is fine, I think local imports aren't allowed in module compilation. Ah, confirmed. So local imports are relative to the source directory, which in this case is maxmcd/bramble. Strange, seems safe to replacecd /home/maxm/go/src/github.com/maxmcd/bramble
withcd .
or similar.- When set up in bramble, we'll be symlinking all of the module source locations into a synthetic directory that is pretending to be the module store or the GOPATH. So directories above like
/home/maxm/go/pkg/mod/github.com/pierrec/lz4/
will be pointed to a temporary directory. We couldreadlink -f
our way to converting those to their real location in the bramble store. gcc
is sometimes called out to, would need to have gcc (and any other programs that are called) available in the build context.$WORK/b305/
is the temporary directory for this specific build. It will be referenced later by other builds.
So if we took the chunk above and did a lot of replacing it could end up like this:
#
# github.com/pierrec/lz4/v3
#
mkdir -p $OUTPUT_b305/
cat >$OUTPUT_b305/go_asm.h << 'EOF' # internal
EOF
cd /home/maxm/bramble/bramble_store_padding/bramble_/icpfggjznz3jxnctxtcky55g7zhbsk4u
/home/maxm/bramble/bramble_store_padding/bramble_/540ihg6hqyyzysnmxbs1smzb4l6s2dz5-go-1.16.9/share/go/pkg/tool/linux_amd64/asm -p github.com/pierrec/lz4/v3 -trimpath "$OUTPUT_b305=>" -I $OUTPUT_b305/ -I /home/maxm/bramble/bramble_store_padding/bramble_/540ihg6hqyyzysnmxbs1smzb4l6s2dz5-go-1.16.9/share/go/pkg/include -D GOOS_linux -D GOARCH_amd64 -gensymabis -o $OUTPUT_b305/symabis ./decode_amd64.s
cat >$OUTPUT_b305/importcfg << 'EOF' # internal
# import config
packagefile encoding/binary=$OUTPUT_b058/_pkg_.a
packagefile errors=$OUTPUT_b004/_pkg_.a
packagefile fmt=$OUTPUT_b020/_pkg_.a
packagefile github.com/pierrec/lz4/v3/internal/xxh32=$OUTPUT_b306/_pkg_.a
packagefile io=$OUTPUT_b029/_pkg_.a
packagefile io/ioutil=$OUTPUT_b044/_pkg_.a
packagefile math/bits=$OUTPUT_b024/_pkg_.a
packagefile os=$OUTPUT_b030/_pkg_.a
packagefile runtime=$OUTPUT_b008/_pkg_.a
packagefile runtime/debug=$OUTPUT_b160/_pkg_.a
packagefile strings=$OUTPUT_b041/_pkg_.a
packagefile sync=$OUTPUT_b014/_pkg_.a
EOF
# cd /home/maxm/go/src/github.com/maxmcd/bramble
/home/maxm/bramble/bramble_store_padding/bramble_/540ihg6hqyyzysnmxbs1smzb4l6s2dz5-go-1.16.9/share/go/pkg/tool/linux_amd64/compile -o $OUTPUT_b305/_pkg_.a -trimpath "$OUTPUT_b305=>" -p github.com/pierrec/lz4/v3 -lang=go1.12 -buildid 5tJja6MFggLI9zzG4-z5/5tJja6MFggLI9zzG4-z5 -goversion go1.16.9 -symabis $OUTPUT_b305/symabis -D "" -importcfg $OUTPUT_b305/importcfg -pack -asmhdr $OUTPUT_b305/go_asm.h -c=4 /home/maxm/bramble/bramble_store_padding/bramble_/icpfggjznz3jxnctxtcky55g7zhbsk4u/block.go /home/maxm/bramble/bramble_store_padding/bramble_/icpfggjznz3jxnctxtcky55g7zhbsk4u/debug_stub.go /home/maxm/bramble/bramble_store_padding/bramble_/icpfggjznz3jxnctxtcky55g7zhbsk4u/decode_amd64.go /home/maxm/bramble/bramble_store_padding/bramble_/icpfggjznz3jxnctxtcky55g7zhbsk4u/errors.go /home/maxm/bramble/bramble_store_padding/bramble_/icpfggjznz3jxnctxtcky55g7zhbsk4u/lz4.go /home/maxm/bramble/bramble_store_padding/bramble_/icpfggjznz3jxnctxtcky55g7zhbsk4u/lz4_go1.10.go /home/maxm/bramble/bramble_store_padding/bramble_/icpfggjznz3jxnctxtcky55g7zhbsk4u/reader.go /home/maxm/bramble/bramble_store_padding/bramble_/icpfggjznz3jxnctxtcky55g7zhbsk4u/writer.go
cd /home/maxm/bramble/bramble_store_padding/bramble_/icpfggjznz3jxnctxtcky55g7zhbsk4u
/home/maxm/bramble/bramble_store_padding/bramble_/540ihg6hqyyzysnmxbs1smzb4l6s2dz5-go-1.16.9/share/go/pkg/tool/linux_amd64/asm -p github.com/pierrec/lz4/v3 -trimpath "$OUTPUT_b305=>" -I $OUTPUT_b305/ -I /home/maxm/bramble/bramble_store_padding/bramble_/540ihg6hqyyzysnmxbs1smzb4l6s2dz5-go-1.16.9/share/go/pkg/include -D GOOS_linux -D GOARCH_amd64 -o $OUTPUT_b305/decode_amd64.o ./decode_amd64.s
/home/maxm/bramble/bramble_store_padding/bramble_/540ihg6hqyyzysnmxbs1smzb4l6s2dz5-go-1.16.9/share/go/pkg/tool/linux_amd64/pack r $OUTPUT_b305/_pkg_.a $OUTPUT_b305/decode_amd64.o # internal
/home/maxm/bramble/bramble_store_padding/bramble_/540ihg6hqyyzysnmxbs1smzb4l6s2dz5-go-1.16.9/share/go/pkg/tool/linux_amd64/buildid -w $OUTPUT_b305/_pkg_.a # internal
Changes:
- We've changed
/home/maxm/go/pkg/mod/github.com/pierrec/lz4/v3@v3.3.2
to a bramble store path. Presumably those files would live in the bramble store for us. - Commented out the
cd
to the project directory. - Replaced
$WORK/b014
with$OUTPUT_b014
, if we decided on a general reference format like this we could pass this script as input to a bramble-post processing script that would turn this into a derivation graph. The output environment variable would be replaced with the correct path in the bramble store. This part is important because the work directory names are effectively random. They could change between different invocations ofgo build -a -n
, so we need to normalize them before they get into the derivation body. - We also need to swap out the filepath references with references to the source derivations.
This could be the whole enchilada. Seems the combination of file generation and graph patching work well.