Skip to content

Commit b381699

Browse files
committed
dxvm: experimental support for Devbox VMs
This is a prototype for using the macOS Virtualization Framework to launch Linux VMs on macOS. See the README.md for details.
1 parent 84d404b commit b381699

File tree

16 files changed

+1252
-9
lines changed

16 files changed

+1252
-9
lines changed

devbox.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,14 @@
33
"go@latest",
44
"golangci-lint@latest"
55
],
6+
"env": {
7+
"AR": "/usr/bin/ar",
8+
"AS": "/usr/bin/as",
9+
"CC": "/usr/bin/cc",
10+
"CXX": "/usr/bin/cxx",
11+
"LD": "/usr/bin/ld",
12+
"NM": "/usr/bin/nm"
13+
},
614
"shell": {
715
"init_hook": [
816
"echo 'Welcome to devbox!' > /dev/null"

devbox.lock

Lines changed: 34 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,44 @@
22
"lockfile_version": "1",
33
"packages": {
44
"go@latest": {
5-
"last_modified": "2023-07-30T12:29:02Z",
6-
"resolved": "github:NixOS/nixpkgs/3acb5c4264c490e7714d503c7166a3fde0c51324#go",
5+
"last_modified": "2023-09-15T06:49:28Z",
6+
"resolved": "github:NixOS/nixpkgs/46688f8eb5cd6f1298d873d4d2b9cf245e09e88e#go_1_21",
77
"source": "devbox-search",
8-
"version": "1.20.6"
8+
"version": "1.21.1",
9+
"systems": {
10+
"aarch64-darwin": {
11+
"store_path": "/nix/store/jkhg33806wygpwpix47d2h5scfgn01i8-go-1.21.1"
12+
},
13+
"aarch64-linux": {
14+
"store_path": "/nix/store/sgkkfw6saficch0mviqyqyw6nj64kzf9-go-1.21.1"
15+
},
16+
"x86_64-darwin": {
17+
"store_path": "/nix/store/w67nj5iqgnz0msi8i12kyh9nhsp2ci9n-go-1.21.1"
18+
},
19+
"x86_64-linux": {
20+
"store_path": "/nix/store/jk0bqfsjijia52vks1wxqnn4s6dxaiqp-go-1.21.1"
21+
}
22+
}
923
},
1024
"golangci-lint@latest": {
11-
"last_modified": "2023-06-30T04:44:22Z",
12-
"resolved": "github:NixOS/nixpkgs/3c614fbc76fc152f3e1bc4b2263da6d90adf80fb#golangci-lint",
25+
"last_modified": "2023-09-15T06:49:28Z",
26+
"resolved": "github:NixOS/nixpkgs/46688f8eb5cd6f1298d873d4d2b9cf245e09e88e#golangci-lint",
1327
"source": "devbox-search",
14-
"version": "1.53.3"
28+
"version": "1.54.2",
29+
"systems": {
30+
"aarch64-darwin": {
31+
"store_path": "/nix/store/xvmmv6mzzpx1krr05zmpkzpc1q6dnlcn-golangci-lint-1.54.2"
32+
},
33+
"aarch64-linux": {
34+
"store_path": "/nix/store/x581cpf86b0jdpmdn8shx8vm2rw4c0qb-golangci-lint-1.54.2"
35+
},
36+
"x86_64-darwin": {
37+
"store_path": "/nix/store/wdrdk7xv3fmblhg1grfw5c665mgnsz9z-golangci-lint-1.54.2"
38+
},
39+
"x86_64-linux": {
40+
"store_path": "/nix/store/9nd1nv54dsmkjy0lad38xrilzh71lvjc-golangci-lint-1.54.2"
41+
}
42+
}
1543
}
1644
}
1745
}

go.work

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
go 1.20
1+
go 1.21.1
22

33
use (
44
./envsec

pkg/go.mod

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
module go.jetpack.io/pkg
22

3-
go 1.20
3+
go 1.21
44

55
require (
6+
github.com/Code-Hex/vz/v3 v3.0.6
67
github.com/cavaliergopher/grab/v3 v3.0.1
78
github.com/codeclysm/extract/v3 v3.1.1
89
github.com/coreos/go-oidc/v3 v3.6.0
@@ -20,9 +21,11 @@ require (
2021
github.com/spf13/cobra v1.7.0
2122
github.com/stretchr/testify v1.8.4
2223
golang.org/x/oauth2 v0.12.0
24+
golang.org/x/sys v0.12.0
2325
)
2426

2527
require (
28+
github.com/Code-Hex/go-infinity-channel v1.0.0 // indirect
2629
github.com/ProtonMail/go-crypto v0.0.0-20230217124315-7d5c6f04bbb8 // indirect
2730
github.com/cloudflare/circl v1.3.3 // indirect
2831
github.com/davecgh/go-spew v1.1.1 // indirect
@@ -43,7 +46,7 @@ require (
4346
github.com/spf13/pflag v1.0.5 // indirect
4447
github.com/ulikunitz/xz v0.5.11 // indirect
4548
golang.org/x/crypto v0.13.0 // indirect
46-
golang.org/x/sys v0.12.0 // indirect
49+
golang.org/x/mod v0.13.0 // indirect
4750
google.golang.org/appengine v1.6.8 // indirect
4851
google.golang.org/protobuf v1.31.0 // indirect
4952
gopkg.in/yaml.v3 v3.0.1 // indirect

pkg/go.sum

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
1+
github.com/Code-Hex/go-infinity-channel v1.0.0 h1:M8BWlfDOxq9or9yvF9+YkceoTkDI1pFAqvnP87Zh0Nw=
2+
github.com/Code-Hex/go-infinity-channel v1.0.0/go.mod h1:5yUVg/Fqao9dAjcpzoQ33WwfdMWmISOrQloDRn3bsvY=
3+
github.com/Code-Hex/vz/v3 v3.0.6 h1:YoW0ZHbdb9G1lYDw9h/QrbBC5lAI1k9LAZMmTGR/Rpw=
4+
github.com/Code-Hex/vz/v3 v3.0.6/go.mod h1:xUfvg1VJ5A6ZQNuzQERwXJ7l2ZdTnY6eCy9CIS6/DYQ=
15
github.com/ProtonMail/go-crypto v0.0.0-20230217124315-7d5c6f04bbb8 h1:wPbRQzjjwFc0ih8puEVAOFGELsn1zoIIYdxvML7mDxA=
26
github.com/ProtonMail/go-crypto v0.0.0-20230217124315-7d5c6f04bbb8/go.mod h1:I0gYDMZ6Z5GRU7l58bNFSkPTFN6Yl12dsUlAZ8xy98g=
37
github.com/arduino/go-paths-helper v1.2.0 h1:qDW93PR5IZUN/jzO4rCtexiwF8P4OIcOmcSgAYLZfY4=
8+
github.com/arduino/go-paths-helper v1.2.0/go.mod h1:HpxtKph+g238EJHq4geEPv9p+gl3v5YYu35Yb+w31Ck=
49
github.com/bwesterb/go-ristretto v1.2.0/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0=
510
github.com/cavaliergopher/grab/v3 v3.0.1 h1:4z7TkBfmPjmLAAmkkAZNX/6QJ1nNFdv3SdIHXju0Fr4=
611
github.com/cavaliergopher/grab/v3 v3.0.1/go.mod h1:1U/KNnD+Ft6JJiYoYBAimKH2XrYptb8Kl3DFGmsjpq4=
@@ -30,6 +35,7 @@ github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
3035
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
3136
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
3237
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
38+
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
3339
github.com/google/go-github/v53 v53.2.0 h1:wvz3FyF53v4BK+AsnvCmeNhf8AkTaeh2SoYu/XUvTtI=
3440
github.com/google/go-github/v53 v53.2.0/go.mod h1:XhFRObz+m/l+UCm9b7KSIC3lT3NWSXGt7mOsAWEloao=
3541
github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
@@ -97,6 +103,8 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y
97103
golang.org/x/crypto v0.13.0 h1:mvySKfSWJ+UKUii46M40LOvyWfN0s2U+46/jDd0e6Ck=
98104
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
99105
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
106+
golang.org/x/mod v0.13.0 h1:I/DsJXRlw/8l/0c24sM9yb0T4z9liZTduXvdAWYiysY=
107+
golang.org/x/mod v0.13.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
100108
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
101109
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
102110
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
@@ -137,6 +145,7 @@ google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqw
137145
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
138146
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
139147
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
148+
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
140149
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
141150
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
142151
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=

pkg/sandbox/vm/README.md

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
# Experimental Devbox VM
2+
3+
Experimental support for Devbox virtual machines on macOS.
4+
5+
## Usage
6+
7+
The `dxvm` command acts like `devbox shell` except that it launches the Devbox
8+
environment in a VM.
9+
10+
To create a new VM, run the following:
11+
12+
cd ~/my/project
13+
dxvm -install
14+
# Wait for the prompt. This might appear to hang the first time it's run
15+
# while downloading the NixOS installer.
16+
17+
mkdir bootstrap
18+
sudo mount -t virtiofs bootstrap bootstrap
19+
sudo bootstrap/install.sh
20+
sudo shutdown now
21+
# ^C to exit
22+
23+
Now that the VM is bootstrapped, you can launch it any time with:
24+
25+
cd ~/my/project
26+
dxvm
27+
28+
The NixOS installer files are cached in `~/.local/state/devbox/vm`. You can
29+
monitor the ISO in this directory to estimate how far along the download is. The
30+
final size should be around 800 MiB.
31+
32+
The first time `dxvm` is run in a Devbox project, it creates a `.devbox/vm`
33+
directory that contains the VM's state and configuration files:
34+
35+
- `log` - error and debug log messages
36+
- `disk.img` - main disk image, typically mounted as root
37+
- `id` - an opaque Virtualization Framework machine ID
38+
39+
The following files can be edited (for example, `echo 4 > cpu`) to adjust the
40+
VM's resources:
41+
42+
- `mem` - the amount of memory (in bytes) to allocate to the VM
43+
- `cpu` - the number of CPUs to allocate to the VM
44+
45+
## Building
46+
47+
This package uses the macOS Virtualization Framework, and therefore needs CGO.
48+
Devbox and Nix are unable to download the macOS SDK directly, so some extra
49+
setup is required:
50+
51+
- macOS Ventura (13) or later
52+
- XCode command line tools (open Xcode at least once to accept the EULA)
53+
54+
To compile and sign `dxvm` run:
55+
56+
devbox run build
57+
58+
The `devbox run build` script uses `./cmd/dxvmsign` to sign the Go binary, which
59+
allows it to use the Virtualization Framework. It's a small wrapper around
60+
Apple's `codesign` utility.
61+
62+
## Limitations
63+
64+
- Mounting the Devbox project directory was temporarily removed while cleaning
65+
things up. Needs to be brought back.
66+
- Only aarch64-linux is implemented right now. Other systems have been tested,
67+
but they aren't an option in the dxvm command.
68+
- Using ctrl-c to exit has the unfortunate side-effect of making it impossible
69+
to interrupt a program in the VM.
70+
- The host terminal has no way of telling the guest when it has resized (usually
71+
this is done with SIGWINCH). Running less/vim/etc. in the VM might look messed
72+
up. Run `stty cols X rows Y` in the VM to manually set the size of your terminal
73+
window.
74+
75+
# Todo/Ideas
76+
77+
- Support macOS and x86_64-linux.
78+
- macOS Sonoma added support for VM suspend/resume. This would probably make VM
79+
start times a lot faster (maybe instant?).
80+
- Clipboard sharing.
81+
- Expose sockets for services.
82+
- Mount /nix/store as an overlay to share packages between VMs.
83+
- Communicate directly with the host Nix daemon?
84+
- Disk resizing.
85+
- Memory balloon (adjust VM memory at runtime).
86+
- Multiple consoles.
87+
88+
## Docs
89+
90+
Some useful links for learning more about the Virtualization Framework:
91+
92+
- `vz` - Go bindings for Apple's Virtualization Framework
93+
- <https://github.com/Code-Hex/vz>
94+
- <https://github.com/Code-Hex/vz/wiki>
95+
- <https://pkg.go.dev/github.com/Code-Hex/vz/v3>
96+
- Virtualization Framework
97+
- <https://developer.apple.com/documentation/virtualization>

pkg/sandbox/vm/bin/dxvm

9.3 MB
Binary file not shown.
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
{ config, pkgs, lib, ... }:
2+
3+
{
4+
boot = {
5+
consoleLogLevel = 0;
6+
kernelParams = [ "quiet" "udev.log_level=3" ];
7+
8+
initrd = {
9+
enable = true;
10+
verbose = false;
11+
};
12+
13+
loader = {
14+
grub.enable = false;
15+
generationsDir = {
16+
enable = true;
17+
copyKernels = true;
18+
};
19+
};
20+
};
21+
22+
environment = {
23+
# defaultPackages = [ ];
24+
systemPackages = with pkgs; [ curl vim ];
25+
};
26+
27+
fileSystems = {
28+
"/" = {
29+
device = "/dev/vda";
30+
fsType = "ext4";
31+
};
32+
};
33+
34+
nix = {
35+
settings = {
36+
auto-optimise-store = true;
37+
experimental-features = [ "ca-derivations" "flakes" "nix-command" "repl-flake" ];
38+
};
39+
};
40+
41+
nixpkgs = {
42+
config = {
43+
allowInsecure = true;
44+
allowUnfree = true;
45+
};
46+
hostPlatform = lib.mkDefault "{{.System}}";
47+
};
48+
49+
security.sudo = {
50+
extraConfig = "Defaults lecture = never";
51+
wheelNeedsPassword = false;
52+
};
53+
54+
services.getty = {
55+
autologinUser = "{{.User.Username}}";
56+
greetingLine = "";
57+
};
58+
59+
system.stateVersion = "23.05";
60+
61+
time.timeZone = "{{.TimeZone}}";
62+
63+
users.users = {
64+
root = {
65+
hashedPassword = "";
66+
};
67+
"{{.User.Username}}" = {
68+
isNormalUser = true;
69+
description = "{{.User.Name}}";
70+
hashedPassword = ""; # passwordless login
71+
extraGroups = [ "wheel" ];
72+
};
73+
};
74+
}

pkg/sandbox/vm/bootstrap/install.sh

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
#!/bin/sh
2+
3+
mkfs.ext4 /dev/vda
4+
mount -t ext4 /dev/vda /mnt
5+
mkdir -p /mnt/nix /mnt/boot /mnt/etc/nixos
6+
mount -t virtiofs boot /mnt/boot
7+
cat << 'EOF' > /mnt/etc/nixos/configuration.nix
8+
{{ template "configuration.nix" . -}}
9+
EOF
10+
nixos-install --no-root-password --show-trace --root /mnt

pkg/sandbox/vm/cmd/dxvm/main.go

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
package main
2+
3+
import (
4+
"context"
5+
"flag"
6+
"log/slog"
7+
"os"
8+
"os/signal"
9+
"time"
10+
11+
"go.jetpack.io/pkg/sandbox/vm"
12+
)
13+
14+
func main() {
15+
ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt)
16+
defer cancel()
17+
18+
vm := vm.VM{}
19+
flag.StringVar(&vm.HostDataDir, "datadir", ".devbox/vm", "`path` to the directory for saving VM state")
20+
flag.BoolVar(&vm.Install, "install", false, "mount NixOS install image")
21+
flag.Parse()
22+
23+
if vm.Install {
24+
slog.Debug("downloading the NixOS installer, this make take a few minutes")
25+
}
26+
27+
err := vm.Start(ctx)
28+
if err != nil {
29+
slog.Error("start virtual machine", "err", err)
30+
os.Exit(1)
31+
}
32+
33+
<-ctx.Done()
34+
ctx, cancel = context.WithTimeout(context.Background(), 10*time.Second)
35+
defer cancel()
36+
if err := vm.Stop(ctx); err != nil {
37+
slog.Error("stop virtual machine", "err", err)
38+
}
39+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3+
<plist version="1.0">
4+
<dict>
5+
<key>com.apple.security.network.server</key>
6+
<true/>
7+
<key>com.apple.security.network.client</key>
8+
<true/>
9+
<key>com.apple.security.virtualization</key>
10+
<true/>
11+
</dict>
12+
</plist>

0 commit comments

Comments
 (0)