diff --git a/go.mod b/go.mod index 223acf9a5aa..940d1624e35 100644 --- a/go.mod +++ b/go.mod @@ -14,8 +14,6 @@ require ( github.com/aws/aws-sdk-go-v2/service/sts v1.28.11 github.com/bmatcuk/doublestar/v4 v4.6.1 github.com/briandowns/spinner v1.23.0 - github.com/cavaliergopher/grab/v3 v3.0.1 - github.com/cloudflare/ahocorasick v0.0.0-20210425175752-730270c3e184 github.com/denisbrodbeck/machineid v1.0.1 github.com/f1bonacc1/process-compose v1.6.1 github.com/fatih/color v1.17.0 @@ -48,7 +46,6 @@ require ( golang.org/x/oauth2 v0.21.0 golang.org/x/sync v0.7.0 golang.org/x/tools v0.22.0 - gopkg.in/natefinch/lumberjack.v2 v2.2.1 gopkg.in/yaml.v3 v3.0.1 ) @@ -78,6 +75,7 @@ require ( github.com/bodgit/sevenzip v1.5.1 // indirect github.com/bodgit/windows v1.0.1 // indirect github.com/buger/jsonparser v1.1.1 // indirect + github.com/cavaliergopher/grab/v3 v3.0.1 // indirect github.com/charmbracelet/lipgloss v0.11.0 // indirect github.com/charmbracelet/x/ansi v0.1.2 // indirect github.com/cloudflare/circl v1.3.8 // indirect diff --git a/go.sum b/go.sum index ee36653737c..be8b71cdb6c 100644 --- a/go.sum +++ b/go.sum @@ -110,8 +110,6 @@ github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWR github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= -github.com/cloudflare/ahocorasick v0.0.0-20210425175752-730270c3e184 h1:8yL+85JpbwrIc6m+7N1iYrjn/22z68jwrTIBOJHNe4k= -github.com/cloudflare/ahocorasick v0.0.0-20210425175752-730270c3e184/go.mod h1:tGWUZLZp9ajsxUOnHmFFLnqnlKXsCn6GReG4jAD59H0= github.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA= github.com/cloudflare/circl v1.3.8 h1:j+V8jJt09PoeMFIu2uh5JUyEaIHTXVOHslFoLNAKqwI= github.com/cloudflare/circl v1.3.8/go.mod h1:PDRU+oXvdD7KCtgKxW95M5Z8BpSCJXQORiZFnBQS5QU= @@ -617,8 +615,6 @@ gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntN gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= -gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc= -gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/internal/boxcli/cloud.go b/internal/boxcli/cloud.go deleted file mode 100644 index 097466705c2..00000000000 --- a/internal/boxcli/cloud.go +++ /dev/null @@ -1,176 +0,0 @@ -// Copyright 2024 Jetify Inc. and contributors. All rights reserved. -// Use of this source code is governed by the license in the LICENSE file. - -package boxcli - -import ( - "fmt" - "strings" - - "github.com/pkg/errors" - "github.com/spf13/cobra" - - "go.jetpack.io/devbox/internal/boxcli/usererr" - "go.jetpack.io/devbox/internal/cloud" - "go.jetpack.io/devbox/internal/devbox" - "go.jetpack.io/devbox/internal/devbox/devopt" - "go.jetpack.io/devbox/internal/envir" -) - -type cloudShellCmdFlags struct { - config configFlags - - githubUsername string -} - -func cloudCmd() *cobra.Command { - command := &cobra.Command{ - Use: "cloud", - Short: "[Preview] Remote development environments on the cloud", - Long: "Remote development environments on the cloud. All cloud commands " + - "are currently in developer preview and may have some rough edges. " + - "Please report any issues to https://github.com/jetify-com/devbox/issues", - Hidden: true, - RunE: func(cmd *cobra.Command, args []string) error { - return cmd.Help() - }, - } - command.AddCommand(cloudShellCmd()) - command.AddCommand(cloudInitCmd()) - command.AddCommand(cloudPortForwardCmd()) - return command -} - -func cloudInitCmd() *cobra.Command { - flags := cloudShellCmdFlags{} - command := &cobra.Command{ - Use: "init", - Hidden: true, - Short: "Create a Cloud VM without connecting to its shell", - RunE: func(cmd *cobra.Command, args []string) error { - return runCloudInit(cmd, &flags) - }, - } - flags.config.register(command) - return command -} - -func cloudShellCmd() *cobra.Command { - flags := cloudShellCmdFlags{} - - command := &cobra.Command{ - Use: "shell", - Short: "[Preview] Shell into a cloud environment that matches your local devbox environment", - RunE: func(cmd *cobra.Command, args []string) error { - return runCloudShellCmd(cmd, &flags) - }, - } - - flags.config.register(command) - command.Flags().StringVarP( - &flags.githubUsername, "username", "u", "", "Github username to use for ssh", - ) - return command -} - -func cloudPortForwardCmd() *cobra.Command { - command := &cobra.Command{ - Use: "forward : | : | stop | list", - Short: "[Preview] Port forward a local port to a remote devbox cloud port", - Long: "Port forward a local port to a remote devbox cloud port. If 0 or " + - "no local port is specified, we find a suitable local port. Use 'stop' " + - "to stop all port forwards.", - Args: cobra.ExactArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - ports := strings.Split(args[0], ":") - - if len(ports) != 2 { - return usererr.New("Invalid port format. Expected :") - } - localPort, err := cloud.PortForward(ports[0], ports[1]) - if err != nil { - return errors.WithStack(err) - } - cmd.PrintErrf( - "Port forwarding %s:%s\nTo view in browser, visit http://localhost:%[1]s\n", - localPort, - ports[1], - ) - return nil - }, - } - command.AddCommand(cloudPortForwardList()) - command.AddCommand(cloudPortForwardStopCmd()) - return command -} - -func cloudPortForwardStopCmd() *cobra.Command { - return &cobra.Command{ - Use: "stop", - Short: "Stop all port forwards managed by devbox", - Args: cobra.ExactArgs(0), - RunE: func(cmd *cobra.Command, args []string) error { - return cloud.PortForwardTerminateAll() - }, - } -} - -func cloudPortForwardList() *cobra.Command { - return &cobra.Command{ - Use: "list", - Aliases: []string{"ls"}, - Short: "List all port forwards managed by devbox", - Args: cobra.ExactArgs(0), - RunE: func(cmd *cobra.Command, args []string) error { - l, err := cloud.PortForwardList() - if err != nil { - return errors.WithStack(err) - } - for _, p := range l { - cmd.Println(p) - } - return nil - }, - } -} - -func runCloudShellCmd(cmd *cobra.Command, flags *cloudShellCmdFlags) error { - // calling `devbox cloud shell` when already in the VM is not allowed. - if envir.IsDevboxCloud() { - return shellInceptionErrorMsg("devbox cloud shell") - } - - box, err := devbox.Open(&devopt.Opts{ - Dir: flags.config.path, - Environment: flags.config.environment, - Stderr: cmd.ErrOrStderr(), - }) - if err != nil { - return errors.WithStack(err) - } - return cloud.Shell(cmd.Context(), cmd.ErrOrStderr(), box.ProjectDir(), flags.githubUsername) -} - -func runCloudInit(cmd *cobra.Command, flags *cloudShellCmdFlags) error { - // calling `devbox cloud init` when already in the VM is not allowed. - if envir.IsDevboxCloud() { - return shellInceptionErrorMsg("devbox cloud init") - } - - box, err := devbox.Open(&devopt.Opts{ - Dir: flags.config.path, - Environment: flags.config.environment, - Stderr: cmd.ErrOrStderr(), - }) - if err != nil { - return errors.WithStack(err) - } - _, vmhostname, _, err := cloud.InitVM(cmd.Context(), cmd.ErrOrStderr(), box.ProjectDir(), flags.githubUsername) - if err != nil { - return err - } - // printing vmHostname so that the output of devbox cloud init can be read by - // devbox extension - fmt.Fprintln(cmd.ErrOrStderr(), vmhostname) - return nil -} diff --git a/internal/boxcli/generate.go b/internal/boxcli/generate.go index 7ac7b83b225..f966cf6f214 100644 --- a/internal/boxcli/generate.go +++ b/internal/boxcli/generate.go @@ -13,7 +13,6 @@ import ( "github.com/spf13/cobra" "go.jetpack.io/devbox/internal/boxcli/usererr" - "go.jetpack.io/devbox/internal/cloud" "go.jetpack.io/devbox/internal/devbox" "go.jetpack.io/devbox/internal/devbox/devopt" "go.jetpack.io/devbox/internal/devbox/docgen" @@ -24,7 +23,6 @@ type generateCmdFlags struct { config configFlags force bool printEnvrcContent bool - githubUsername string rootUser bool } @@ -61,7 +59,6 @@ func generateCmd() *cobra.Command { command.AddCommand(debugCmd()) command.AddCommand(direnvCmd()) command.AddCommand(genReadmeCmd()) - command.AddCommand(sshConfigCmd()) flags.config.register(command) return command @@ -158,27 +155,6 @@ func direnvCmd() *cobra.Command { return command } -func sshConfigCmd() *cobra.Command { - flags := &generateCmdFlags{} - command := &cobra.Command{ - Use: "ssh-config", - Hidden: true, - Short: "Generate ssh config to connect to devbox cloud", - Long: "Check ssh config and if they don't exist, it generates the configs necessary to connect to devbox cloud VMs.", - Args: cobra.MaximumNArgs(0), - RunE: func(cmd *cobra.Command, args []string) error { - // ssh-config command is exception and it should run without a config file present - _, err := cloud.SSHSetup(flags.githubUsername) - return errors.WithStack(err) - }, - } - command.Flags().StringVarP( - &flags.githubUsername, "username", "u", "", "GitHub username to use for ssh", - ) - flags.config.register(command) - return command -} - func genReadmeCmd() *cobra.Command { flags := &GenerateReadmeCmdFlags{} diff --git a/internal/boxcli/root.go b/internal/boxcli/root.go index e30ba6a363b..c502915aa52 100644 --- a/internal/boxcli/root.go +++ b/internal/boxcli/root.go @@ -15,7 +15,6 @@ import ( "go.jetpack.io/devbox/internal/boxcli/featureflag" "go.jetpack.io/devbox/internal/boxcli/midcobra" - "go.jetpack.io/devbox/internal/cloud/openssh/sshshim" "go.jetpack.io/devbox/internal/cmdutil" "go.jetpack.io/devbox/internal/debug" "go.jetpack.io/devbox/internal/telemetry" @@ -82,8 +81,6 @@ func RootCmd() *cobra.Command { })) command.AddCommand(updateCmd()) command.AddCommand(versionCmd()) - // Preview commands - command.AddCommand(cloudCmd()) // Internal commands command.AddCommand(genDocsCmd()) @@ -120,10 +117,6 @@ func Main() { timer := debug.Timer(strings.Join(os.Args, " ")) setSystemBinaryPaths() ctx := context.Background() - if strings.HasSuffix(os.Args[0], "ssh") || - strings.HasSuffix(os.Args[0], "scp") { - os.Exit(sshshim.Execute(ctx, os.Args)) - } if len(os.Args) > 1 && os.Args[1] == "upload-telemetry" { // This subcommand is hidden and only run by devbox itself as a diff --git a/internal/cloud/cloud.go b/internal/cloud/cloud.go deleted file mode 100644 index 3ecc3888fcc..00000000000 --- a/internal/cloud/cloud.go +++ /dev/null @@ -1,599 +0,0 @@ -// Copyright 2024 Jetify Inc. and contributors. All rights reserved. -// Use of this source code is governed by the license in the LICENSE file. - -package cloud - -import ( - "context" - "encoding/json" - "fmt" - "io" - "io/fs" - "log/slog" - "os" - "os/exec" - "path/filepath" - "strings" - "time" - - "github.com/AlecAivazis/survey/v2" - "github.com/fatih/color" - "github.com/pkg/errors" - - "go.jetpack.io/devbox/internal/boxcli/usererr" - "go.jetpack.io/devbox/internal/cloud/fly" - "go.jetpack.io/devbox/internal/cloud/mutagen" - "go.jetpack.io/devbox/internal/cloud/mutagenbox" - "go.jetpack.io/devbox/internal/cloud/openssh" - "go.jetpack.io/devbox/internal/cloud/openssh/sshshim" - "go.jetpack.io/devbox/internal/envir" - "go.jetpack.io/devbox/internal/services" - "go.jetpack.io/devbox/internal/telemetry" - "go.jetpack.io/devbox/internal/ux/stepper" -) - -func SSHSetup(username string) (*openssh.Cmd, error) { - sshCmd := &openssh.Cmd{ - Username: username, - DestinationAddr: "gateway.devbox.sh", - } - // When developing we can use this env variable to point - // to a different gateway - var err error - if envGateway := os.Getenv(envir.DevboxGateway); envGateway != "" { - sshCmd.DestinationAddr = envGateway - err = openssh.SetupInsecureDebug(envGateway) - } else { - err = openssh.SetupDevbox() - } - if err != nil { - return nil, err - } - if err := sshshim.Setup(); err != nil { - return nil, err - } - return sshCmd, nil -} - -func ensureVMForUser(vmHostname string, w io.Writer, username string, sshCmd *openssh.Cmd) (string, error) { - if vmHostname == "" { - color.New(color.FgGreen).Fprintln(w, "Creating a virtual machine on the cloud...") - // Inspect the ssh ControlPath to check for existing connections - vmHostname = vmHostnameFromSSHControlPath() - if vmHostname != "" { - slog.Debug("Using vmHostname from ssh socket", "host", vmHostname) - color.New(color.FgGreen).Fprintln(w, "Detected existing virtual machine") - } else { - var region, vmUser string - vmUser, hostname, region, err := getVirtualMachine(sshCmd) - if err != nil { - return "", err - } - if vmUser != "" { - username = vmUser - } - vmHostname = hostname - color.New(color.FgGreen).Fprintf(w, "Created a virtual machine in %s\n", fly.RegionName(region)) - - // We save the username to local file only after we get a successful response - // from the gateway, because the gateway will verify that the user's SSH keys - // match their claimed username from GitHub. - err = openssh.SaveGithubUsernameToLocalFile(username) - if err != nil { - slog.Error("failed to save username", "err", err) - } - } - } - return vmHostname, nil -} - -func Shell(ctx context.Context, w io.Writer, projectDir, githubUsername string) error { - color.New(color.FgMagenta, color.Bold).Fprint(w, "Devbox Cloud\n") - fmt.Fprint(w, "Remote development environments powered by Nix\n\n") - fmt.Fprint(w, "This is an open developer preview and may have some rough edges. Please report any issues to https://github.com/jetify-com/devbox/issues\n\n") - - username, vmHostname, telemetryShellStartTime, err := InitVM(ctx, w, projectDir, githubUsername) - if err != nil { - return err - } - // file sync and shell - color.New(color.FgGreen).Fprintln(w, "Starting file syncing...") - err = syncFiles(username, vmHostname, projectDir) - if err != nil { - color.New(color.FgRed).Fprintln(w, "Starting file syncing [FAILED]") - return err - } - color.New(color.FgGreen).Fprintln(w, "File syncing started") - - s3 := stepper.Start(w, "Connecting to virtual machine...") - time.Sleep(1 * time.Second) - s3.Stop("Connecting to virtual machine") - fmt.Fprint(w, "\n") - - hostID := strings.Split(vmHostname, ".")[0] - if err = AutoPortForward(ctx, w, projectDir, hostID); err != nil { - return err - } - - return shell(username, vmHostname, projectDir, telemetryShellStartTime) -} - -// Temporary function to create a vm and print vmHostname to be used by devbox extension -func InitVM( - ctx context.Context, - w io.Writer, - projectDir string, - githubUsername string, -) (string, string, time.Time, error) { - var nilTime time.Time - if err := ensureProjectDirIsNotSensitive(projectDir); err != nil { - return "", "", nilTime, err - } - username, vmHostname := parseVMEnvVar() - // The flag for githubUsername overrides any env-var, since flags are a more - // explicit action compared to an env-var which could be latently present. - if githubUsername != "" { - username = githubUsername - } - if username == "" { - var err error - username, err = getGithubUsername() - if err != nil { - return "", "", nilTime, err - } - } - slog.Debug("initializing vm", "user", username) - - // Record the start time for telemetry, now that we are done with prompting - // for GitHub username. - telemetryShellStartTime := time.Now() - // setup ssh config - sshCmd, err := SSHSetup(username) - if err != nil { - return "", "", nilTime, err - } - - // creating vm for user if it doesn't exist - vmHostname, err = ensureVMForUser(vmHostname, w, username, sshCmd) - if err != nil { - return "", "", nilTime, err - } - slog.Debug("initializing vm", "host", vmHostname) - - return username, vmHostname, telemetryShellStartTime, nil -} - -func PortForward(local, remote string) (string, error) { - vmHostname := vmHostnameFromSSHControlPath() - if vmHostname == "" { - return "", usererr.New("No VM found. Please run `devbox cloud shell` first.") - } - return mutagenbox.ForwardCreate(vmHostname, local, remote) -} - -func PortForwardTerminateAll() error { - return mutagenbox.ForwardTerminateAll() -} - -func PortForwardList() ([]string, error) { - return mutagenbox.ForwardList() -} - -func AutoPortForward(ctx context.Context, w io.Writer, projectDir, hostID string) error { - return services.ListenToChanges(ctx, - &services.ListenerOpts{ - HostID: hostID, - ProjectDir: projectDir, - Writer: w, - UpdateFunc: func(service *services.ServiceStatus) (*services.ServiceStatus, bool) { - if service == nil { - return service, false - } - host := vmHostnameFromSSHControlPath() - if host == "" { - return service, false - } - - saveChanges := false - if service.Running && service.Port != "" { - localPort, err := mutagenbox.ForwardCreateIfNotExists(host, "", service.Port) - if err != nil { - fmt.Fprintf(w, "Failed to create port forward for %s: %v", service.Name, err) - } - if service.LocalPort != localPort { - service.LocalPort = localPort - saveChanges = true - } - } else if service.Port != "" { - if err := mutagenbox.ForwardTerminateByHostPort(host, service.Port); err != nil { - fmt.Fprintf(w, "Failed to terminate port forward for %s: %v", service.Name, err) - } - if service.LocalPort != "" { - service.LocalPort = "" - saveChanges = true - } - } - return service, saveChanges - }, - }, - ) -} - -func getGithubUsername() (string, error) { - username, err := openssh.GithubUsernameFromLocalFile() - if err == nil && username != "" { - slog.Debug("got username from locally-cached file", "user", username) - return username, nil - } - - if err != nil { - slog.Debug("failed to get auth.Username", "err", err) - } - username, err = queryGithubUsername() - if err == nil && username != "" { - slog.Debug("got username from ssh -T git@github.com", "user", username) - return username, nil - } - - // The query for GitHub username is best effort, and if it fails to resolve - // we fallback to prompting the user, and suggesting the local computer username. - if err != nil { - slog.Debug("failed to query auth.Username", "err", err) - } - return promptUsername() -} - -func promptUsername() (string, error) { - username := "" - prompt := &survey.Input{ - Message: "What is your github username?", - Default: os.Getenv(envir.User), - } - err := survey.AskOne(prompt, &username, survey.WithValidator(survey.Required)) - if err != nil { - return "", errors.WithStack(err) - } - slog.Debug("got username from prompt", "user", username) - return username, nil -} - -type vm struct { - JumpHost string `json:"jump_host"` - JumpHostPort int `json:"jump_host_port"` - VMHost string `json:"vm_host"` - VMHostPort int `json:"vm_host_port"` - VMRegion string `json:"vm_region"` - VMUsername string `json:"vm_username"` - VMPublicKey string `json:"vm_public_key"` - VMPrivateKey string `json:"vm_private_key"` -} - -func (vm vm) redact() *vm { - vm.VMPrivateKey = "***" - return &vm -} - -func getVirtualMachine(sshCmd *openssh.Cmd) (vmUser, vmHost, region string, err error) { - sshOut, err := sshCmd.ExecRemote("auth") - if err != nil { - return "", "", "", errors.Wrapf(err, "error requesting VM") - } - resp := &vm{} - if err := json.Unmarshal(sshOut, resp); err != nil { - return "", "", "", errors.Wrapf(err, "error unmarshalling gateway response %q", sshOut) - } - if redacted, err := json.MarshalIndent(resp.redact(), "\t", " "); err == nil { - slog.Debug("got gateway response", "resp", redacted) - } - if resp.VMPrivateKey != "" { - err = openssh.AddVMKey(resp.VMHost, resp.VMPrivateKey) - if err != nil { - return "", "", "", errors.Wrapf(err, "error adding new VM key") - } - } - return resp.VMUsername, resp.VMHost, resp.VMRegion, nil -} - -func syncFiles(username, hostname, projectDir string) error { - relProjectPathInVM, err := relativeProjectPathInVM(projectDir) - if err != nil { - return err - } - absPathInVM := absoluteProjectPathInVM(username, relProjectPathInVM) - slog.Debug("syncFiles absoluteProjectPathInVM", "path", absPathInVM) - - err = copyConfigFileToVM(hostname, username, projectDir, absPathInVM) - if err != nil { - return err - } - - env, err := mutagenbox.DefaultEnv() - if err != nil { - return err - } - - ignorePaths, err := gitIgnorePaths(projectDir) - if err != nil { - return err - } - - // TODO: instead of id, have the server return the machine's name and use that - // here to. It'll make things easier to debug. - machineID, _, _ := strings.Cut(hostname, ".") - mutagenSessionName := mutagen.SanitizeSessionName(fmt.Sprintf("devbox-%s-%s", machineID, - hyphenatePath(relProjectPathInVM))) - - _, err = mutagen.Sync(&mutagen.SessionSpec{ - // If multiple projects can sync to the same machine, we need the name to also include - // the project's id. - Name: mutagenSessionName, - AlphaPath: projectDir, - BetaAddress: fmt.Sprintf("%s@%s", username, hostname), - // It's important that the beta path is a "clean" directory that will contain *only* - // the projects files. If we pick a pre-existing directories with other files, those - // files will be synced back to the local directory (due to two-way-sync) and pollute - // the user's local project - BetaPath: absPathInVM, - EnvVars: env, - Ignore: mutagen.SessionIgnore{ - VCS: true, - Paths: ignorePaths, - }, - SyncMode: "two-way-resolved", - Labels: mutagenbox.DefaultSyncLabels(machineID), - }) - if err != nil { - return err - } - time.Sleep(1 * time.Second) - - // In a background routine, update the sync status in the cloud VM - go updateSyncStatus(mutagenSessionName, username, hostname, relProjectPathInVM) - return nil -} - -// updateSyncStatus updates the starship prompt. -// -// wait for the mutagen session's status to change to "watching", and update the remote VM -// when the initial project sync completes and then exit. -func updateSyncStatus(mutagenSessionName, username, hostname, relProjectPathInVM string) { - status := "disconnected" - - // Ensure the destination directory exists - destDir := fmt.Sprintf("/home/%s/.config/devbox/starship/%s", username, hyphenatePath(filepath.Base(relProjectPathInVM))) - mkdirCmd := openssh.Command(username, hostname) - _, err := mkdirCmd.ExecRemote(fmt.Sprintf(`mkdir -p "%s"`, destDir)) - if err != nil { - slog.Error("error setting initial starship mutagen status", "err", err) - } - - // Set an initial status - displayableStatus := "initial sync" - statusCmd := openssh.Command(username, hostname) - _, err = statusCmd.ExecRemote(fmt.Sprintf(`echo "%s" > "%s/mutagen_status.txt"`, displayableStatus, destDir)) - if err != nil { - slog.Error("error setting initial starship mutagen status", "err", err) - } - time.Sleep(5 * time.Second) - - slog.Debug("Starting check for file sync status") - for status != "watching" { - status, err = getSyncStatus(mutagenSessionName) - if err != nil { - slog.Error("getSyncStatus error", "err", err) - return - } - slog.Debug("checking file sync status", "status", status) - - if status == "watching" { - displayableStatus = "\"watching for changes\"" - } - - statusCmd := openssh.Command(username, hostname) - _, err = statusCmd.ExecRemote(fmt.Sprintf(`echo "%s" > "%s/mutagen_status.txt"`, displayableStatus, destDir)) - if err != nil { - slog.Error("error setting initial starship mutagen status", "err", err) - } - time.Sleep(5 * time.Second) - } -} - -func getSyncStatus(mutagenSessionName string) (string, error) { - env, err := mutagenbox.DefaultEnv() - if err != nil { - return "", errors.WithStack(err) - } - sessions, err := mutagen.List(env, mutagenSessionName) - if err != nil { - return "", errors.WithStack(err) - } - if len(sessions) == 0 { - return "", errors.WithStack(err) - } - return sessions[0].Status, nil -} - -func copyConfigFileToVM(hostname, username, projectDir, pathInVM string) error { - // Ensure the devbox-project's directory exists in the VM - mkdirCmd := openssh.Command(username, hostname) - // This is the first command we run on the VM. Sometimes is takes fly.io a few seconds - // to propagate DNS, especially if the VM is located in a different region than - // the proxy (this can happen if the gateway is in a different region to proxy) - // We retry a few times to avoid failing the command. - _, err := mkdirCmd.ExecRemoteWithRetry(fmt.Sprintf(`mkdir -p "%s"`, pathInVM), 5, 4) - if err != nil { - slog.Error("error copying config file to VM", "err", err) - return errors.WithStack(err) - } - - // Copy the config file to the devbox-project directory in the VM - destServer := fmt.Sprintf("%s@%s", username, hostname) - configFilePath := filepath.Join(projectDir, "devbox.json") - destPath := fmt.Sprintf("%s:%s", destServer, pathInVM) - cmd := exec.Command("scp", configFilePath, destPath) - err = cmd.Run() - slog.Error("scp devbox.json error", "cmd", cmd, "err", err) - return errors.WithStack(err) -} - -func shell(username, hostname, projectDir string, shellStartTime time.Time) error { - projectPath, err := relativeProjectPathInVM(projectDir) - if err != nil { - return err - } - - cmd := &openssh.Cmd{ - DestinationAddr: hostname, - PathInVM: absoluteProjectPathInVM(username, projectPath), - ShellStartTime: telemetry.FormatShellStart(shellStartTime), - Username: username, - } - sessionErrors := newSSHSessionErrors() - return cloudShellErrorHandler(cmd.Shell(sessionErrors), sessionErrors) -} - -// relativeProjectPathInVM refers to the project path relative to the user's -// home-directory within the VM. -// -// Ideally, we'd pass in devbox.Devbox struct and call ProjectDir but it -// makes it hard to wrap this in a test -func relativeProjectPathInVM(projectDir string) (string, error) { - home, err := os.UserHomeDir() - if err != nil { - return "", errors.WithStack(err) - } - - // get absProjectDir to expand "." and so on - absProjectDir, err := filepath.Abs(projectDir) - if err != nil { - return "", errors.WithStack(err) - } - projectDir = filepath.Clean(absProjectDir) - - if !strings.HasPrefix(projectDir, home) { - projectDir, err = filepath.Abs(projectDir) - if err != nil { - return "", errors.WithStack(err) - } - return filepath.Join(outsideHomedirDirectory, projectDir), nil - } - - relativeProjectDir, err := filepath.Rel(home, projectDir) - if err != nil { - return "", errors.WithStack(err) - } - return relativeProjectDir, nil -} - -const outsideHomedirDirectory = "outside-homedir-code" - -func absoluteProjectPathInVM(sshUser, relativeProjectPath string) string { - vmHomeDir := fmt.Sprintf("/home/%s", sshUser) - if strings.HasPrefix(relativeProjectPath, outsideHomedirDirectory) { - return fmt.Sprintf("%s/%s", vmHomeDir, relativeProjectPath) - } - return fmt.Sprintf("%s/%s/", vmHomeDir, relativeProjectPath) -} - -func parseVMEnvVar() (username, vmHostname string) { - vmEnvVar := os.Getenv(envir.DevboxVM) - if vmEnvVar == "" { - return "", "" - } - parts := strings.Split(vmEnvVar, "@") - - // DEVBOX_VM = - if len(parts) == 1 { - vmHostname = parts[0] - return username, vmHostname - } - - // DEVBOX_VM = @ - username = parts[0] - vmHostname = parts[1] - return username, vmHostname -} - -// Proof of concept: look for a gitignore file in the current directory. -// To harden this, we must: -// 1. Look for .gitignore file in each ancestor directory of projectDir, and include -// any rules that apply to projectDir contents. -// 2. Look for .gitignore file in each child directory of projectDir and transform the -// rules to be relative to projectDir. -func gitIgnorePaths(projectDir string) ([]string, error) { - // We must always ignore .devbox folder. It can contain information that - // is platform-specific, and so we should not sync it to the cloud-shell. - // Platform-specific info includes nix profile links to the nix store, - // and in the future, versions of specific packages in the flakes.lock file. - result := []string{".devbox"} - - fpath := filepath.Join(projectDir, ".gitignore") - if _, err := os.Stat(fpath); err != nil { - if errors.Is(err, fs.ErrNotExist) { - return result, nil - } - return nil, errors.WithStack(err) - } - - contents, err := os.ReadFile(fpath) - if err != nil { - return nil, errors.WithStack(err) - } - - for _, line := range strings.Split(string(contents), "\n") { - line = strings.TrimSpace(line) - if !strings.HasPrefix(line, "#") && line != "" { - result = append(result, line) - } - } - - return result, nil -} - -func vmHostnameFromSSHControlPath() string { - for _, socket := range openssh.DevboxControlSockets() { - if strings.HasSuffix(socket.Host, "vm.devbox-vms.internal") { - return socket.Host - } - } - // empty string means that aren't any active VM connections - return "" -} - -func hyphenatePath(path string) string { - return strings.ReplaceAll(path, "/", "-") -} - -func ensureProjectDirIsNotSensitive(dir string) error { - // isSensitiveDir checks if the dir is the rootdir or the user's homedir - isSensitiveDir := func(dir string) bool { - dir = filepath.Clean(dir) - if dir == "/" { - return true - } - - home, err := os.UserHomeDir() - if err != nil { - return false - } - return dir == filepath.Clean(home) - } - - if isSensitiveDir(dir) { - // check for a git repository in this folder before using this project config - // (and potentially syncing all the code to jetify-cloud) - _, err := os.Stat(filepath.Join(dir, ".git")) - if err != nil { - if errors.Is(err, fs.ErrNotExist) { - return usererr.New( - "Found a config (devbox.json) file at %s, "+ - "but since it is a sensitive directory we require it to be part of a git repository "+ - "before we sync it to jetify cloud", - dir, - ) - } - return errors.WithStack(err) - } - } - return nil -} diff --git a/internal/cloud/cloud_test.go b/internal/cloud/cloud_test.go deleted file mode 100644 index 11a225cbc02..00000000000 --- a/internal/cloud/cloud_test.go +++ /dev/null @@ -1,51 +0,0 @@ -// Copyright 2024 Jetify Inc. and contributors. All rights reserved. -// Use of this source code is governed by the license in the LICENSE file. - -package cloud - -import ( - "os" - "path/filepath" - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestProjectDirName(t *testing.T) { - assertion := assert.New(t) - - homeDir, err := os.UserHomeDir() - assertion.NoError(err) - - workingDir, err := os.Getwd() - assertion.NoError(err) - - relWorkingDir, err := filepath.Rel(homeDir, workingDir) - assertion.NoError(err) - - testCases := []struct { - projectDir string - dirPath string - }{ - // inside homedir - {".", relWorkingDir}, - {filepath.Join(homeDir, "foo"), "foo"}, - {filepath.Join(homeDir, "foo/bar"), "foo/bar"}, - - // non-home-dir - {"/", filepath.Join(outsideHomedirDirectory, "/")}, - {"/foo", filepath.Join(outsideHomedirDirectory, "/foo")}, - {"/foo/bar", filepath.Join(outsideHomedirDirectory, "/foo/bar")}, - {"/foo/bar/", filepath.Join(outsideHomedirDirectory, "/foo/bar")}, - {"/foo/bar///", filepath.Join(outsideHomedirDirectory, "/foo/bar")}, - } - - for _, testCase := range testCases { - t.Run(testCase.projectDir, func(t *testing.T) { - assert := assert.New(t) - path, err := relativeProjectPathInVM(testCase.projectDir) - assert.NoError(err) - assert.Equal(testCase.dirPath, path) - }) - } -} diff --git a/internal/cloud/errors.go b/internal/cloud/errors.go deleted file mode 100644 index 2ae1129bc02..00000000000 --- a/internal/cloud/errors.go +++ /dev/null @@ -1,67 +0,0 @@ -// Copyright 2024 Jetify Inc. and contributors. All rights reserved. -// Use of this source code is governed by the license in the LICENSE file. - -package cloud - -import ( - "io" - "strings" - - "go.jetpack.io/devbox/internal/boxcli/usererr" -) - -const errApplyNixDerivationString = "Error: apply Nix derivation:" - -var sshSessionErrorStrings = []string{ - errApplyNixDerivationString, -} - -// sshSessionErrors is a helper struct to collect errors from ssh sessions. -// For performance and privacy it doesn't actually keep any content from the -// sessions, but instead just keeps track of which errors were encountered. -type sshSessionErrors struct { - errors map[string]bool -} - -var _ io.Writer = (*sshSessionErrors)(nil) - -func newSSHSessionErrors() *sshSessionErrors { - return &sshSessionErrors{ - errors: make(map[string]bool), - } -} - -func (s *sshSessionErrors) Write(p []byte) (n int, err error) { - for _, errorString := range sshSessionErrorStrings { - if strings.Contains(string(p), errorString) { - s.errors[errorString] = true - } - } - return len(p), nil -} - -// cloudShellErrorHandler is a helper function to handle ssh errors that -// may contain nix errors in them. For now being cautious and logging them -// to Sentry even though they may be due to user action. -func cloudShellErrorHandler(err error, sessionErrors *sshSessionErrors) error { - if err == nil { - return nil - } - - // This usually on initial setup when running start_devbox_shell.sh - if found := sessionErrors.errors[errApplyNixDerivationString]; found { - return usererr.WithLoggedUserMessage( - err, - "Failed to apply Nix derivation. This can happen if your devbox (nix) "+ - "packages don't exist or failed to build. Please check your "+ - "devbox.json and try again", - ) - } - - // This can happen due to connection issues or any other unforeseen errors - return usererr.WithLoggedUserMessage( - err, - "Your cloud shell terminated unexpectedly. Please check your connection "+ - "and devbox.json and try again", - ) -} diff --git a/internal/cloud/fly/region.go b/internal/cloud/fly/region.go deleted file mode 100644 index 89fe5865c8e..00000000000 --- a/internal/cloud/fly/region.go +++ /dev/null @@ -1,40 +0,0 @@ -// Copyright 2024 Jetify Inc. and contributors. All rights reserved. -// Use of this source code is governed by the license in the LICENSE file. - -package fly - -func RegionName(code string) string { - if name, ok := regions[code]; ok { - return name - } - return code -} - -var regions = map[string]string{ - "ams": "Amsterdam, Netherlands", - "cdg": "Paris, France", - "den": "Denver, Colorado (US)", - "dfw": "Dallas, Texas (US)", - "ewr": "Secaucus, NJ (US)", - "fra": "Frankfurt, Germany", - "gru": "São Paulo", - "hkg": "Hong Kong, Hong Kong", - "iad": "Ashburn, Virginia (US)", - "jnb": "Johannesburg, South Africa", - "lax": "Los Angeles, California (US)", - "lhr": "London, United Kingdom", - "maa": "Chennai (Madras), India", - "mad": "Madrid, Spain", - "mia": "Miami, Florida (US)", - "nrt": "Tokyo, Japan", - "ord": "Chicago, Illinois (US)", - "otp": "Bucharest, Romania", - "scl": "Santiago, Chile", - "sea": "Seattle, Washington (US)", - "sin": "Singapore", - "sjc": "Sunnyvale, California (US)", - "syd": "Sydney, Australia", - "waw": "Warsaw, Poland", - "yul": "Montreal, Canada", - "yyz": "Toronto, Canada", -} diff --git a/internal/cloud/mutagen/forward.go b/internal/cloud/mutagen/forward.go deleted file mode 100644 index 7c0e96d5218..00000000000 --- a/internal/cloud/mutagen/forward.go +++ /dev/null @@ -1,45 +0,0 @@ -// Copyright 2024 Jetify Inc. and contributors. All rights reserved. -// Use of this source code is governed by the license in the LICENSE file. - -package mutagen - -import ( - "encoding/json" - - "github.com/pkg/errors" -) - -type Forward struct { - Source struct { - Connected bool `json:"connected"` - Endpoint string `json:"endpoint"` - } `json:"source"` - Destination struct { - Endpoint string `json:"endpoint"` - } `json:"destination"` - LastError string `json:"lastError"` -} - -// ForwardCreate creates a new port forward using mutagen. -// local looks like tcp:127.0.0.1: -// remote looks like ::tcp:: (ssh-port is usually 22) -func ForwardCreate(env map[string]string, local, remote string, labels map[string]string) error { - args := []string{"forward", "create", local, remote} - return execMutagenEnv(append(args, labelFlag(labels)...), env) -} - -func ForwardTerminate(env, labels map[string]string) error { - args := []string{"forward", "terminate"} - return execMutagenEnv(append(args, labelSelectorFlag(labels)...), env) -} - -func ForwardList(env, labels map[string]string) ([]Forward, error) { - args := []string{"forward", "list", "--template", "{{json .}}"} - out, err := execMutagenOut(append(args, labelSelectorFlag(labels)...), env) - if err != nil { - return nil, err - } - - list := []Forward{} - return list, errors.WithStack(json.Unmarshal(out, &list)) -} diff --git a/internal/cloud/mutagen/install.go b/internal/cloud/mutagen/install.go deleted file mode 100644 index 89655f720ff..00000000000 --- a/internal/cloud/mutagen/install.go +++ /dev/null @@ -1,75 +0,0 @@ -// Copyright 2024 Jetify Inc. and contributors. All rights reserved. -// Use of this source code is governed by the license in the LICENSE file. - -package mutagen - -import ( - "fmt" - "log/slog" - "os" - "path/filepath" - "runtime" - - "github.com/cavaliergopher/grab/v3" - - "go.jetpack.io/devbox/internal/fileutil" -) - -func InstallMutagenOnce(binPath string) error { - if fileutil.IsFile(binPath) { - // Already installed, do nothing - // TODO: ideally we would check that the right version - // is installed, and maybe we should also validate - // with a checksum. - return nil - } - - url := mutagenURL() - installDir := filepath.Dir(binPath) - - return Install(url, installDir) -} - -func Install(url, installDir string) error { - slog.Debug("installing mutagen from %s to %s", url, installDir) - err := os.MkdirAll(installDir, 0o755) - if err != nil { - return err - } - - // TODO: add checksum validation - resp, err := grab.Get(os.TempDir(), url) - if err != nil { - return err - } - - tarPath := resp.Filename - tarReader, err := os.Open(tarPath) - if err != nil { - return err - } - return fileutil.Untar(tarReader, installDir) -} - -func mutagenURL() string { - repo := "mutagen-io/mutagen" - pkg := "mutagen" - version := "v0.16.2" // Hard-coded for now, but change to always get the latest? - platform := detectPlatform() - - return fmt.Sprintf("https://github.com/%s/releases/download/%s/%s_%s_%s.tar.gz", repo, version, pkg, platform, version) -} - -func detectOS() string { - return runtime.GOOS -} - -func detectArch() string { - return runtime.GOARCH -} - -func detectPlatform() string { - os := detectOS() - arch := detectArch() - return fmt.Sprintf("%s_%s", os, arch) -} diff --git a/internal/cloud/mutagen/sync.go b/internal/cloud/mutagen/sync.go deleted file mode 100644 index 44ccdb0ad00..00000000000 --- a/internal/cloud/mutagen/sync.go +++ /dev/null @@ -1,48 +0,0 @@ -// Copyright 2024 Jetify Inc. and contributors. All rights reserved. -// Use of this source code is governed by the license in the LICENSE file. - -package mutagen - -import ( - "errors" -) - -func Sync(spec *SessionSpec) (*Session, error) { - if spec.Name == "" { - return nil, errors.New("name is required") - } - - // Check if there's an existing sessions or not - sessions, err := List(spec.EnvVars, spec.Name) - if err != nil { - return nil, err - } - - // If there isn't, create a new one - if len(sessions) == 0 { - err = Create(spec) - if err != nil { - return nil, err - } - } - // Whether new or pre-existing, find the sessions object, ensure - // that it's not paused, and return it. - sessions, err = List(spec.EnvVars, spec.Name) - if err != nil { - return nil, err - } - for _, session := range sessions { - // TODO: should we handle errors for Reset and Resume differently? - _ = Reset(spec.EnvVars, session.Identifier) - _ = Resume(spec.EnvVars, session.Identifier) - } - if len(sessions) > 0 { - return &sessions[0], nil - } - return nil, errors.New("failed to find session that was just created") - // TODO: starting the mutagen session currently fails if there's any error or - // interactivity required for the ssh connection. - // That includes: - // - When connecting for the first time and adding the host to known_hosts - // - When the key has changed and SSH warns of a man-in-the-middle attack -} diff --git a/internal/cloud/mutagen/type_test.go b/internal/cloud/mutagen/type_test.go deleted file mode 100644 index b1e0635da55..00000000000 --- a/internal/cloud/mutagen/type_test.go +++ /dev/null @@ -1,32 +0,0 @@ -// Copyright 2024 Jetify Inc. and contributors. All rights reserved. -// Use of this source code is governed by the license in the LICENSE file. - -package mutagen - -import ( - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestSanitizeSessionName(t *testing.T) { - testCases := []struct { - input string - sanitized string - }{ - {"7foo", "a7foo"}, - {"foo", "foo"}, - {"foo/bar", "foo-bar"}, - {"foo/bar/baz", "foo-bar-baz"}, - {"foo.bar", "foo-bar"}, - {"foo_bar", "foo-bar"}, - } - - for _, testCase := range testCases { - t.Run(testCase.input, func(t *testing.T) { - assert := assert.New(t) - result := SanitizeSessionName(testCase.input) - assert.Equal(testCase.sanitized, result) - }) - } -} diff --git a/internal/cloud/mutagen/types.go b/internal/cloud/mutagen/types.go deleted file mode 100644 index 57bf6ae9c00..00000000000 --- a/internal/cloud/mutagen/types.go +++ /dev/null @@ -1,89 +0,0 @@ -// Copyright 2024 Jetify Inc. and contributors. All rights reserved. -// Use of this source code is governed by the license in the LICENSE file. - -package mutagen - -import ( - "errors" - "unicode" - "unicode/utf8" -) - -type SessionIgnore struct { - VCS bool - Paths []string -} - -type SessionSpec struct { - AlphaAddress string - AlphaPath string - BetaAddress string - BetaPath string - Name string - Labels map[string]string - Paused bool - SyncMode string - Ignore SessionIgnore - EnvVars map[string]string -} - -func (s *SessionSpec) Validate() error { - if s.AlphaPath == "" { - return errors.New("alpha path is required") - } - if s.BetaPath == "" { - return errors.New("beta path is required") - } - return nil -} - -// SanitizeSessionName ensures the input string contains letter, number or dash -// runes. This matches the implementation in mutagen's codebase: -// https://github.com/mutagen-io/mutagen/blob/master/pkg/selection/names.go -// -// TODO savil. Refactor SessionSpec so that this is always applied. -// We can make it a struct that uses a constructor, and make Sync a method on the struct. -func SanitizeSessionName(input string) string { - result := make([]byte, 0, len(input)) - - // note that for-range over a string extracts characters of type rune - for index, char := range input { - // the first character must be a letter - if index == 0 && !unicode.IsLetter(char) { - result = utf8.AppendRune(result, 'a') - } - - if unicode.IsLetter(char) || unicode.IsNumber(char) || char == '-' { - result = utf8.AppendRune(result, char) - } else { - result = utf8.AppendRune(result, '-') - } - } - return string(result) -} - -// Based on the structs available at: https://github.com/mutagen-io/mutagen/blob/master/pkg/api/models/synchronization/session.go -// These contain a subset of fields. - -type Session struct { - Identifier string `json:"identifier"` - Version uint32 `json:"version"` - CreationTime string `json:"creationTime"` - CreatingVersion string `json:"creatingVersion"` - Alpha Endpoint `json:"alpha"` - Beta Endpoint `json:"beta"` - Name string `json:"name,omitempty"` - Labels map[string]string `json:"labels,omitempty"` - Paused bool `json:"paused"` - Status string `json:"status"` -} - -type Endpoint struct { - User string `json:"user,omitempty"` - Host string `json:"host,omitempty"` - Port uint16 `json:"port,omitempty"` - Path string `json:"path"` - Environment map[string]string `json:"environment,omitempty"` - Parameters map[string]string `json:"parameters,omitempty"` - Connected bool `json:"connected"` -} diff --git a/internal/cloud/mutagen/wrapper.go b/internal/cloud/mutagen/wrapper.go deleted file mode 100644 index 15a49edbcf7..00000000000 --- a/internal/cloud/mutagen/wrapper.go +++ /dev/null @@ -1,248 +0,0 @@ -// Copyright 2024 Jetify Inc. and contributors. All rights reserved. -// Use of this source code is governed by the license in the LICENSE file. - -package mutagen - -import ( - "bytes" - "encoding/json" - "fmt" - "log/slog" - "os" - "os/exec" - "strings" - - "github.com/pkg/errors" - - "go.jetpack.io/devbox/internal/xdg" -) - -func Create(spec *SessionSpec) error { - err := spec.Validate() - if err != nil { - return err - } - - alpha := spec.AlphaPath - if spec.AlphaAddress != "" { - alpha = fmt.Sprintf("%s:%s", spec.AlphaAddress, spec.AlphaPath) - } - - beta := spec.BetaPath - if spec.BetaAddress != "" { - beta = fmt.Sprintf("%s:%s", spec.BetaAddress, spec.BetaPath) - } - - args := []string{"sync", "create", alpha, beta} - if spec.Name != "" { - args = append(args, "--name", spec.Name) - } - if spec.Paused { - args = append(args, "--paused") - } - - for k, v := range spec.Labels { - args = append(args, "--label", fmt.Sprintf("%s=%s", k, v)) - } - - if spec.SyncMode == "" { - args = append(args, "--sync-mode", "two-way-resolved") - } else { - args = append(args, "--sync-mode", spec.SyncMode) - } - - if spec.Ignore.VCS { - args = append(args, "--ignore-vcs") - } - - if len(spec.Ignore.Paths) > 0 { - for _, p := range spec.Ignore.Paths { - args = append(args, "--ignore", p) - } - } - - return execMutagenEnv(args, spec.EnvVars) -} - -func List(envVars map[string]string, names ...string) ([]Session, error) { - binPath := ensureMutagen() - cmd := exec.Command(binPath, "sync", "list", "--template", "{{json .}}") - cmd.Args = append(cmd.Args, names...) - cmd.Env = envAsKeyValueStrings(envVars) - - debugPrintExecCmd(cmd) - out, err := cmd.CombinedOutput() - if err != nil { - slog.Debug("list error", "err", err, "out", string(out)) - if e := (&exec.ExitError{}); errors.As(err, &e) { - errMsg := strings.TrimSpace(string(out)) - // Special handle the case where no sessions are found: - if strings.Contains(errMsg, "unable to locate requested sessions") { - return []Session{}, nil - } - return nil, errors.New(errMsg) - } - return nil, err - } - - sessions := []Session{} - err = json.Unmarshal(out, &sessions) - if err != nil { - return nil, err - } - - return sessions, nil -} - -func Pause(names ...string) error { - args := []string{"sync", "pause"} - args = append(args, names...) - return execMutagen(args) -} - -func Resume(envVars map[string]string, names ...string) error { - args := []string{"sync", "resume"} - args = append(args, names...) - return execMutagenEnv(args, envVars) -} - -func Flush(names ...string) error { - args := []string{"sync", "flush"} - args = append(args, names...) - return execMutagen(args) -} - -func Reset(envVars map[string]string, names ...string) error { - args := []string{"sync", "reset"} - args = append(args, names...) - return execMutagenEnv(args, envVars) -} - -func Terminate(env, labels map[string]string, names ...string) error { - args := []string{"sync", "terminate"} - - for k, v := range labels { - args = append(args, "--label-selector", fmt.Sprintf("%s=%s", k, v)) - } - - args = append(args, names...) - return execMutagenEnv(args, env) -} - -func execMutagen(args []string) error { - return execMutagenEnv(args, nil) -} - -func execMutagenEnv(args []string, envVars map[string]string) error { - _, err := execMutagenOut(args, envVars) - return err -} - -func execMutagenOut(args []string, envVars map[string]string) ([]byte, error) { - binPath := ensureMutagen() - cmd := exec.Command(binPath, args...) - cmd.Env = envAsKeyValueStrings(envVars) - - var stdout bytes.Buffer - var stderr bytes.Buffer - cmd.Stdout = &stdout - cmd.Stderr = &stderr - - debugPrintExecCmd(cmd) - - if err := cmd.Run(); err != nil { - slog.Debug( - "execMutagen error", - "err", err, - "stdout", stdout.String(), - "stderr", stderr.String(), - ) - if e := (&exec.ExitError{}); errors.As(err, &e) { - return nil, errors.New(strings.TrimSpace(stderr.String())) - } - return nil, err - } - - slog.Debug("execMutagen worked for cmd", "cmd", cmd) - return stdout.Bytes(), nil -} - -// debugPrintExecCmd prints the command to be run, along with MUTAGEN env-vars -func debugPrintExecCmd(cmd *exec.Cmd) { - envPrint := "" - for _, cmdEnv := range cmd.Env { - if strings.HasPrefix(cmdEnv, "MUTAGEN") { - envPrint = fmt.Sprintf("%s, %s", envPrint, cmdEnv) - } - } - slog.Debug("running mutagen cmd %s with MUTAGEN env: %s", cmd.String(), envPrint) -} - -// envAsKeyValueStrings prepares the env-vars in key=value format to add to the command to be run -// -// panics if os.Environ() returns an array with any element not in key=value format -func envAsKeyValueStrings(userEnv map[string]string) []string { - if userEnv == nil { - userEnv = map[string]string{} - } - - // Convert env to map, and strip out MUTAGEN_PROMPTER env-var - envMap := map[string]string{} - for _, envVar := range os.Environ() { - k, v, found := strings.Cut(envVar, "=") - if !found { - panic(fmt.Sprintf("did not find an = in env-var: %s", envVar)) - } - // Mutagen sets this variable for ssh/scp scenarios, which then expect interactivity? - // https://github.com/mutagen-io/mutagen/blob/b97ff3764a6a6cb91b48ad27def078f6d6a76e24/cmd/mutagen/main.go#L89-L94 - // - // We do not include MUTAGEN_PROMPTER, otherwise mutagen-CLI rejects the command we are about to invoke, - // by treating it instead as a prompter-command. - if k != "MUTAGEN_PROMPTER" { - envMap[k] = v - } - } - - // userEnv overrides the default env - for k, v := range userEnv { - envMap[k] = v - } - - // Convert the envMap to an envList - envList := make([]string, 0, len(envMap)) - for k, v := range envMap { - envList = append(envList, fmt.Sprintf("%s=%s", k, v)) - } - return envList -} - -func ensureMutagen() string { - installPath := xdg.CacheSubpath("mutagen/bin/mutagen") - err := InstallMutagenOnce(installPath) - if err != nil { - panic(err) - } - return installPath -} - -func labelFlag(labels map[string]string) []string { - if len(labels) == 0 { - return []string{} - } - labelSlice := []string{} - for k, v := range labels { - labelSlice = append(labelSlice, fmt.Sprintf("%s=%s", k, v)) - } - return []string{"--label", strings.Join(labelSlice, ",")} -} - -func labelSelectorFlag(labels map[string]string) []string { - if len(labels) == 0 { - return []string{} - } - labelSlice := []string{} - for k, v := range labels { - labelSlice = append(labelSlice, fmt.Sprintf("%s=%s", k, v)) - } - return []string{"--label-selector", strings.Join(labelSlice, ",")} -} diff --git a/internal/cloud/mutagenbox/doc.go b/internal/cloud/mutagenbox/doc.go deleted file mode 100644 index f12e89e924b..00000000000 --- a/internal/cloud/mutagenbox/doc.go +++ /dev/null @@ -1,11 +0,0 @@ -// Copyright 2024 Jetify Inc. and contributors. All rights reserved. -// Use of this source code is governed by the license in the LICENSE file. - -package mutagenbox - -// mutagenbox is a package that encapsulates state and logic specific to how -// we need to manage mutagen for the devbox cloud. -// -// Also, resolves some compile cycles: -// - [cloud] depends on [mutagenbox], [sshshim], and [mutagen]. -// - [sshshim] depends on [mutagenbox] and [mutagen]. diff --git a/internal/cloud/mutagenbox/forward.go b/internal/cloud/mutagenbox/forward.go deleted file mode 100644 index 009ce2e60a3..00000000000 --- a/internal/cloud/mutagenbox/forward.go +++ /dev/null @@ -1,142 +0,0 @@ -// Copyright 2024 Jetify Inc. and contributors. All rights reserved. -// Use of this source code is governed by the license in the LICENSE file. - -package mutagenbox - -import ( - "fmt" - "net" - "strings" - - "github.com/pkg/errors" - "github.com/samber/lo" - "go.jetpack.io/devbox/internal/boxcli/usererr" - "go.jetpack.io/devbox/internal/cloud/mutagen" -) - -func ForwardCreate(host, localPort, remotePort string) (string, error) { - var err error - if localPort == "" || localPort == "0" { - localPort, err = getFreePort() - if err != nil { - return "", err - } - } - - if !isPortAvailable(localPort) { - return "", usererr.New("Port %s is not available", localPort) - } - - local := "tcp:127.0.0.1:" + localPort - remote := host + ":22:tcp::" + remotePort - labels := map[string]string{ - "devbox": "true", - "remote-host": host, - "remote-port": remotePort, - } - env, err := DefaultEnv() - if err != nil { - return "", err - } - return localPort, mutagen.ForwardCreate(env, local, remote, labels) -} - -func ForwardCreateIfNotExists(host, localPort, remotePort string) (string, error) { - forwards, err := forwardListWithLabels(map[string]string{ - "remote-host": host, - "remote-port": remotePort, - }) - if err != nil { - return "", err - } - if len(forwards) > 0 { - srcParts := strings.Split(forwards[0].Source.Endpoint, ":") - return srcParts[len(srcParts)-1], nil - } - return ForwardCreate(host, localPort, remotePort) -} - -func ForwardTerminateAll() error { - env, err := DefaultEnv() - if err != nil { - return err - } - return mutagen.ForwardTerminate(env, map[string]string{"devbox": "true"}) -} - -func ForwardTerminateByHost(host string) error { - env, err := DefaultEnv() - if err != nil { - return err - } - return mutagen.ForwardTerminate(env, map[string]string{ - "devbox": "true", - "remote-host": host, - }) -} - -func ForwardTerminateByHostPort(host, port string) error { - env, err := DefaultEnv() - if err != nil { - return err - } - return mutagen.ForwardTerminate(env, map[string]string{ - "devbox": "true", - "remote-host": host, - "remote-port": port, - }) -} - -func ForwardList() ([]string, error) { - forwards, err := forwardListWithLabels(map[string]string{}) - if err != nil { - return nil, err - } - - result := []string{} - for _, item := range forwards { - srcParts := strings.Split(item.Source.Endpoint, ":") - destParts := strings.Split(item.Destination.Endpoint, ":") - result = append(result, fmt.Sprintf( - "%s:%s connected: %t %s", - srcParts[len(srcParts)-1], - destParts[len(destParts)-1], - item.Source.Connected, - lo.Ternary(item.LastError != "", "Error: "+item.LastError, ""), - )) - } - - return result, nil -} - -func forwardListWithLabels(labels map[string]string) ([]mutagen.Forward, error) { - env, err := DefaultEnv() - if err != nil { - return nil, err - } - labels["devbox"] = "true" // Add this to all labels - return mutagen.ForwardList(env, labels) -} - -func isPortAvailable(port string) bool { - ln, err := net.Listen("tcp", net.JoinHostPort("localhost", port)) - if err != nil { - return false - } - _ = ln.Close() - return true -} - -func getFreePort() (string, error) { - addr, err := net.ResolveTCPAddr("tcp", "localhost:0") - if err != nil { - return "", errors.WithStack(err) - } - - l, err := net.ListenTCP("tcp", addr) - if err != nil { - return "", errors.WithStack(err) - } - defer l.Close() - return fmt.Sprintf("%d", l.Addr().(*net.TCPAddr).Port), nil -} diff --git a/internal/cloud/mutagenbox/mutagenbox.go b/internal/cloud/mutagenbox/mutagenbox.go deleted file mode 100644 index 30133b90e4d..00000000000 --- a/internal/cloud/mutagenbox/mutagenbox.go +++ /dev/null @@ -1,69 +0,0 @@ -// Copyright 2024 Jetify Inc. and contributors. All rights reserved. -// Use of this source code is governed by the license in the LICENSE file. - -package mutagenbox - -import ( - "os" - "path/filepath" - - "github.com/pkg/errors" - "go.jetpack.io/devbox/internal/cloud/mutagen" -) - -const ( - // relative to user home i.e. ~ - dataDirPath = ".config/devbox/mutagen" -) - -// TerminateSessionsForMachine is a devbox-specific API that calls the generic mutagen terminate API. -// It relies on the mutagen-sync-session's labels to identify which sessions to terminate for -// a particular machine (fly VM). -func TerminateSessionsForMachine(machineID string, userEnv map[string]string) error { - labels := DefaultSyncLabels(machineID) - - env, err := DefaultEnv() - if err != nil { - return err - } - // the user-specified env-vars get precedence over the defaultEnvVars - for k, v := range userEnv { - env[k] = v - } - - return mutagen.Terminate(env, labels) -} - -func DefaultSyncLabels(machineID string) map[string]string { - return map[string]string{ - "devbox-vm": machineID, - } -} - -func DefaultEnv() (map[string]string, error) { - shimDir, err := ShimDir() - if err != nil { - return nil, err - } - - mutagenDir, err := createAndGetDataDir() - if err != nil { - return nil, err - } - - return map[string]string{ - "MUTAGEN_SSH_PATH": shimDir, - "MUTAGEN_DATA_DIRECTORY": mutagenDir, - }, nil -} - -// createAndGetDataDir prepares the data directory for devbox's mutagen instance -func createAndGetDataDir() (string, error) { - home, err := os.UserHomeDir() - if err != nil { - return "", errors.WithStack(err) - } - - path := filepath.Join(home, dataDirPath) - return path, errors.WithStack(os.MkdirAll(path, 0o700)) -} diff --git a/internal/cloud/mutagenbox/shim.go b/internal/cloud/mutagenbox/shim.go deleted file mode 100644 index af2dc15f482..00000000000 --- a/internal/cloud/mutagenbox/shim.go +++ /dev/null @@ -1,22 +0,0 @@ -// Copyright 2024 Jetify Inc. and contributors. All rights reserved. -// Use of this source code is governed by the license in the LICENSE file. - -package mutagenbox - -import ( - "os" - "path/filepath" - - "github.com/pkg/errors" -) - -const shimDirPath = ".config/devbox/ssh/shims" - -func ShimDir() (string, error) { - home, err := os.UserHomeDir() - if err != nil { - return "", errors.WithStack(err) - } - shimDir := filepath.Join(home, shimDirPath) - return shimDir, nil -} diff --git a/internal/cloud/openssh/cmd.go b/internal/cloud/openssh/cmd.go deleted file mode 100644 index 0a12bf7f9a1..00000000000 --- a/internal/cloud/openssh/cmd.go +++ /dev/null @@ -1,176 +0,0 @@ -// Copyright 2024 Jetify Inc. and contributors. All rights reserved. -// Use of this source code is governed by the license in the LICENSE file. - -package openssh - -import ( - "bytes" - "fmt" - "io" - "io/fs" - "log/slog" - "math" - "net" - "os" - "os/exec" - "path/filepath" - "strconv" - "time" -) - -type Cmd struct { - // DestinationAddr is a "hostname[:port]" that specifies the remote host - // and port to connect to. - DestinationAddr string - - // Username is the remote login name. - Username string - - PathInVM string - ShellStartTime string // unix timestamp -} - -func Command(user, dest string) *Cmd { - return &Cmd{DestinationAddr: dest, Username: user} -} - -func (c *Cmd) Shell(w io.Writer) error { - cmd := c.cmd("-t") - remoteCmd := fmt.Sprintf( - `bash -l -c "start_devbox_shell.sh \"%s\" %s"`, - c.PathInVM, - c.ShellStartTime, - ) - cmd.Args = append(cmd.Args, remoteCmd) - cmd.Stdin = os.Stdin - cmd.Stdout = io.MultiWriter(os.Stdout, w) - cmd.Stderr = io.MultiWriter(os.Stderr, w) - return logCmdRun(cmd) -} - -func (c *Cmd) ExecRemote(cmd string) ([]byte, error) { - sshCmd := c.cmd("-T") - sshCmd.Args = append(sshCmd.Args, cmd) - - var stdout, stderr bytes.Buffer - sshCmd.Stdout = &stdout - sshCmd.Stderr = &stderr - - err := logCmdRun(sshCmd) - logCmdOutput(sshCmd, "stderr", stderr.Bytes()) - if err != nil { - // Only log output if there was an error, otherwise we might log - // a VM's private key. - logCmdOutput(sshCmd, "stdout", stdout.Bytes()) - return nil, err - } - return stdout.Bytes(), nil -} - -// ExecRemoteWithRetry runs the given command on the remote host, retrying -// with an exponential backoff if the command fails. maxWait is the maximum -// seconds we wait in between retries. -func (c *Cmd) ExecRemoteWithRetry(cmd string, retries, maxWait int) ([]byte, error) { - var err error - var stdout []byte - for i := 0; i < (retries + 1); i++ { - if stdout, err = c.ExecRemote(cmd); err == nil { - break - } - wait := int(math.Min(float64(maxWait), math.Pow(2, float64(i)))) - slog.Debug("retrying ExecRemote", "err", err, "wait", wait) - time.Sleep(time.Duration(wait) * time.Second) - } - return stdout, err -} - -func (c *Cmd) cmd(sshArgs ...string) *exec.Cmd { - host, port := splitHostPort(c.DestinationAddr) - cmd := exec.Command("ssh", "-l", c.Username) - if port != 0 && port != 22 { - cmd.Args = append(cmd.Args, "-p", strconv.Itoa(port)) - } - cmd.Args = append(cmd.Args, sshArgs...) - cmd.Args = append(cmd.Args, host) - return cmd -} - -// splitHostPort is like net.SplitHostPort except it defaults to port 22 if the -// port in the address is missing or invalid. -func splitHostPort(addr string) (host string, port int) { - host, portStr, err := net.SplitHostPort(addr) - if err != nil { - return addr, 22 - } - port, err = net.LookupPort("tcp", portStr) - if err != nil { - return host, 22 - } - return host, port -} - -func logCmdRun(cmd *exec.Cmd) error { - // Use cmd.Start so we can log the pid. Don't bother writing errors to - // the debug log since those will be printed anyway. - if err := cmd.Start(); err != nil { - return fmt.Errorf("openssh: start command %q: %w", cmd, err) - } - slog.Debug("openssh: started process", "pid", cmd.Process.Pid, "cmd", cmd) - - if err := cmd.Wait(); err != nil { - return fmt.Errorf("openssh: process %d with command %q: %w", - cmd.Process.Pid, cmd, err) - } - slog.Debug("openssh: process exited", "pid", cmd.Process.Pid, "cmd", cmd, "code", 0) - return nil -} - -func logCmdOutput(cmd *exec.Cmd, stdstream string, out []byte) { - out = bytes.TrimSpace(out) - if len(out) == 0 { - slog.Debug("openssh: process exited", "pid", cmd.Process.Pid, "cmd", cmd, "code", cmd.ProcessState.ExitCode(), stdstream, out) - return - } - - out = bytes.ReplaceAll(out, []byte{'\n'}, []byte{'\n', '\t'}) - max := 1 << 16 // 64 KiB - if overflow := len(out) - max; overflow > 0 { - out = bytes.TrimSpace(out[:max]) - if overflow == 1 { - out = append(out, "...truncated 1 byte."...) - } else { - out = fmt.Appendf(out, "...truncated %d bytes.", overflow) - } - } - slog.Debug("openssh: process exited", "pid", cmd.Process.Pid, "cmd", cmd, "code", cmd.ProcessState.ExitCode(), stdstream, out) -} - -type ControlSocket struct { - Path string - Host string -} - -func DevboxControlSockets() []ControlSocket { - socketsDir, err := devboxSocketsDir() - if err != nil { - return nil - } - - // Look through whatever entries we got, even if there was an error. - entries, _ := os.ReadDir(socketsDir) - sockets := make([]ControlSocket, 0, len(entries)) - for _, entry := range entries { - isSocket := (entry.Type() & fs.ModeSocket) == fs.ModeSocket - if isSocket { - sockets = append(sockets, ControlSocket{ - Path: filepath.Join(socketsDir, entry.Name()), - - // Right now the host is just the name, but this - // will need to be updated if ControlPath in - // sshconfig.tmpl ever changes. - Host: entry.Name(), - }) - } - } - return sockets -} diff --git a/internal/cloud/openssh/config.go b/internal/cloud/openssh/config.go deleted file mode 100644 index 703133baab1..00000000000 --- a/internal/cloud/openssh/config.go +++ /dev/null @@ -1,382 +0,0 @@ -// Copyright 2024 Jetify Inc. and contributors. All rights reserved. -// Use of this source code is governed by the license in the LICENSE file. - -package openssh - -import ( - "bufio" - _ "embed" - "fmt" - "io" - "io/fs" - "os" - "path/filepath" - "regexp" - "text/template" - - "github.com/pkg/errors" - "go.jetpack.io/devbox/internal/fileutil" -) - -// These must match what's in sshConfigTmpl. We should eventually make the hosts -// a template variable. -const ( - gatewayProdHost = "gateway.devbox.sh" - gatewayDevHost = "gateway.dev.devbox.sh" -) - -//go:embed sshconfig.tmpl -var sshConfigText string -var sshConfigTmpl = template.Must(template.New("sshconfig").Parse(sshConfigText)) - -//go:embed known_hosts -var sshKnownHosts []byte - -// SetupDevbox updates the user's OpenSSH configuration so that they can connect -// to Devbox Cloud hosts. It does nothing if Devbox Cloud is already -// configured. -func SetupDevbox() error { - return setupDevbox("", 0) -} - -// SetupInsecureDebug is like SetupDevbox, but also configures an additional -// gateway with host key checking disabled. If gatewayAddr is a -// well-known *.devbox.sh gateway, then SetupInsecureDebug doesn't add any -// extra hosts and acts identically to SetupDevbox. -func SetupInsecureDebug(gatewayAddr string) error { - host, port := splitHostPort(gatewayAddr) - if host != gatewayProdHost && host != gatewayDevHost { - return setupDevbox(host, port) - } - return setupDevbox("", 0) -} - -func setupDevbox(debugHost string, debugPort int) error { - devboxSSHDir, err := devboxSSHDir() - if err != nil { - return err - } - - // Ensure ~/.config/devbox/ssh/sockets exists. - if _, err := devboxSocketsDir(); err != nil { - return err - } - - // Try to remove any old debug host keys. It's okay if this fails. - devboxKnownHostsDebug := filepath.Join(devboxSSHDir, "known_hosts_debug") - _ = os.Remove(devboxKnownHostsDebug) - - devboxKnownHostsPath := filepath.Join(devboxSSHDir, "known_hosts") - devboxKnownHosts, err := editFile(devboxKnownHostsPath, 0o644) - if err != nil { - return err - } - defer devboxKnownHosts.Close() - if _, err := devboxKnownHosts.Write(sshKnownHosts); err != nil { - return err - } - if err := devboxKnownHosts.Commit(); err != nil { - return err - } - - devboxIncludePath := filepath.Join(devboxSSHDir, "config") - devboxSSHConfig, err := editFile(devboxIncludePath, 0o644) - if err != nil { - return err - } - defer devboxSSHConfig.Close() - - tmplData := struct { - ConfigVersion string - ConfigDir string - DebugGateway struct { - Host string - Port int - } - }{ - ConfigVersion: "0.0.1", - ConfigDir: devboxSSHDir, - } - tmplData.DebugGateway.Host = debugHost - tmplData.DebugGateway.Port = debugPort - err = errors.WithStack(sshConfigTmpl.Execute(devboxSSHConfig, tmplData)) - if err != nil { - return errors.WithStack(err) - } - if err := devboxSSHConfig.Commit(); err != nil { - return err - } - if err := updateUserSSHConfig(devboxIncludePath); err != nil { - return err - } - - // Create the known_hosts_debug file with the correct permissions if a - // debug gateway is configured. It's okay if this fails because it's - // only used for debugging. - if debugHost != "" { - f, err := os.OpenFile(devboxKnownHostsDebug, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0o644) - if err == nil { - f.Close() - } - } - return nil -} - -// AddVMKey sets the private SSH key for the given Devbox VM host. If a key was -// previously set for the host, AddVMKey replaces it with the new key. The old -// key is not recoverable. -// -// AddVMKey only manages keys specific to Devbox Cloud. It will not touch any of -// the user's keys in ~/.ssh. -func AddVMKey(hostname, key string) error { - keysDir, err := devboxKeysDir() - if err != nil { - return err - } - keyFile, err := editFile(filepath.Join(keysDir, hostname), 0o600) - if err != nil { - return err - } - defer keyFile.Close() - - if _, err := io.WriteString(keyFile, key); err != nil { - return err - } - return keyFile.Commit() -} - -func updateUserSSHConfig(devboxIncludePath string) (err error) { - home, err := os.UserHomeDir() - if err != nil { - return errors.WithStack(err) - } - dotSSH := filepath.Join(home, ".ssh") - if err := EnsureDirExists(dotSSH, 0o700, true); err != nil { - return err - } - - sshConfig, err := editFile(filepath.Join(dotSSH, "config"), 0o644) - if err != nil { - return err - } - defer func() { - closeErr := sshConfig.Close() - if err == nil { - err = closeErr - } - }() - - bufw := bufio.NewWriter(sshConfig) - _, err = fmt.Fprintf(bufw, "Include \"%s\"\n", devboxIncludePath) - if err != nil { - return err - } - - // Look for an existing Include directive, copying the file contents as - // we read. - if containsDevboxInclude(io.TeeReader(sshConfig, bufw)) { - // We found an existing Include - don't save and return. - return nil - } - // We didn't find an existing Include - copy the rest of the user's SSH - // config and then commit the changes. - if _, err := bufw.ReadFrom(sshConfig); err != nil { - return errors.WithStack(err) - } - if err := bufw.Flush(); err != nil { - return errors.WithStack(err) - } - return sshConfig.Commit() -} - -var ( - reDevboxInclude = regexp.MustCompile(`(?i)^[ \t]*"?Include"?[ \t=][^#]*devbox/ssh/config`) - reHostOrMatch = regexp.MustCompile(`(?i)[ \t]*"?(Host|Match) `) -) - -func containsDevboxInclude(r io.Reader) bool { - scanner := bufio.NewScanner(r) - for scanner.Scan() { - line := scanner.Bytes() - if reDevboxInclude.Match(line) { - return true - } - - // Unconditional Include directives must come before any Host or - // Match blocks. If we found one of those blocks then we've gone - // too far. - if reHostOrMatch.Match(line) { - return false - } - } - return false -} - -func EnsureDirExists(path string, perm fs.FileMode, chmod bool) error { - return fileutil.EnsureDirExists(path, perm, chmod) -} - -// returns path to ~/.config/devbox/ssh -func devboxSSHDir() (string, error) { - home, err := os.UserHomeDir() - if err != nil { - return "", errors.WithStack(err) - } - - // Ensure ~/.config exists but don't touch existing permissions. - dotConfig := filepath.Join(home, ".config") - if err := EnsureDirExists(dotConfig, 0o755, false); err != nil { - return "", err - } - - // Ensure ~/.config/devbox exists and force permissions to 0755. - devboxConfigDir := filepath.Join(dotConfig, "devbox") - if err := EnsureDirExists(devboxConfigDir, 0o755, true); err != nil { - return "", err - } - - // Ensure ~/.config/devbox/ssh exists and force permissions to 0700. - devboxSSHDir := filepath.Join(devboxConfigDir, "ssh") - if err := EnsureDirExists(devboxSSHDir, 0o700, true); err != nil { - return "", err - } - return devboxSSHDir, nil -} - -func devboxKeysDir() (string, error) { - sshDir, err := devboxSSHDir() - if err != nil { - return "", err - } - keysDir := filepath.Join(sshDir, "keys") - if err := EnsureDirExists(keysDir, 0o700, true); err != nil { - return "", err - } - return keysDir, nil -} - -func devboxSocketsDir() (string, error) { - sshDir, err := devboxSSHDir() - if err != nil { - return "", err - } - sockets := filepath.Join(sshDir, "sockets") - if err := EnsureDirExists(sockets, 0o700, true); err != nil { - return "", err - } - return sockets, nil -} - -// atomicEdit reads from a source file and writes changes to a separate -// temporary file. Upon a call to Commit, it atomically overwrites the source -// file with the temp file, guaranteeing that all of the file Writes succeed or -// none at all. Calling Close before calling Commit discards any written data, -// leaving the source file untouched. -type atomicEdit struct { - path string - editFile *os.File - tmpFile *os.File - - closed bool - err error -} - -// editFile opens the file at path for editing. Writes to atomicEdit will not -// modify the file until Commit is called. If the file doesn't exist, calls to -// Read immediately return io.EOF and Commit will create it with permissions -// perm. If the file does exist, Commit atomically applies any written data and -// changes its permissions to perm. -// -// Calling Close without calling Commit discards all written data. It is -// unnecessary but valid to call Close after Commit. This makes it easier to -// defer closing the file. -func editFile(path string, perm os.FileMode) (*atomicEdit, error) { - // editFile will be nil when creating a new file. - editFile, err := os.Open(path) - if err != nil && !errors.Is(err, fs.ErrNotExist) { - return nil, errors.WithStack(err) - } - - // Atomic file renames require that both files are on the same volume. - // Putting the tmp file in the same directory is the best way to ensure - // that happens. - tmp, err := os.CreateTemp(filepath.Dir(path), ".devbox") - if err != nil { - return nil, errors.WithStack(err) - } - - // Make sure to set permissions before writing anything. This also means - // perm must be user-writeable. - if err := tmp.Chmod(perm); err != nil { - return nil, errors.WithStack(err) - } - return &atomicEdit{ - path: path, - editFile: editFile, - tmpFile: tmp, - }, nil -} - -func (a *atomicEdit) Read(p []byte) (n int, err error) { - if a.editFile == nil { - return 0, io.EOF - } - n, err = a.editFile.Read(p) - - // Don't use `errors.Is` here because we only want to avoid wrapping - // io.EOF directly. This is for compatibility with the io.Writer - // interface. - // nolint:errorlint - if err != nil && err != io.EOF { - err = errors.WithStack(err) - } - return n, err -} - -func (a *atomicEdit) Write(p []byte) (n int, err error) { - n, err = a.tmpFile.Write(p) - - // Don't use `errors.Is` here because we only want to avoid wrapping - // io.EOF directly. This is for compatibility with the io.Writer - // interface. - // nolint:errorlint - if err != nil && err != io.EOF { - err = errors.WithStack(err) - } - return n, err -} - -func (a *atomicEdit) Commit() error { - if a.closed { - return a.err - } - a.closed = true - - if a.editFile != nil { - // Ignore close errors - we only ever read from the original - // file. - a.editFile.Close() - } - if a.err = errors.WithStack(a.tmpFile.Close()); a.err != nil { - return a.err - } - if a.err = errors.WithStack(os.Rename(a.tmpFile.Name(), a.path)); a.err != nil { - return a.err - } - return a.err -} - -func (a *atomicEdit) Close() error { - if a.closed { - return a.err - } - a.closed = true - - // Ignore close errors - we're throwing away any changes. - if a.editFile != nil { - a.editFile.Close() - } - a.tmpFile.Close() - a.err = errors.WithStack(os.Remove(a.tmpFile.Name())) - return a.err -} diff --git a/internal/cloud/openssh/config_test.go b/internal/cloud/openssh/config_test.go deleted file mode 100644 index c4b357c7687..00000000000 --- a/internal/cloud/openssh/config_test.go +++ /dev/null @@ -1,408 +0,0 @@ -// Copyright 2024 Jetify Inc. and contributors. All rights reserved. -// Use of this source code is governed by the license in the LICENSE file. - -package openssh - -import ( - "bytes" - _ "embed" - "io/fs" - "os" - "path/filepath" - "testing" - "testing/fstest" - - "github.com/google/go-cmp/cmp" - "github.com/pkg/errors" - - "go.jetpack.io/devbox/internal/envir" -) - -func TestDevboxIncludeRegex(t *testing.T) { - tests := map[string]bool{ - `Include ~/.config/devbox/ssh/config`: true, - `include ~/.config/devbox/ssh/config`: true, - `Include "~/.config/devbox/ssh/config"`: true, - `"Include" ~/.config/devbox/ssh/config`: true, - `"Include" "~/.config/devbox/ssh/config"`: true, - `"Include" = "~/.config/devbox/ssh/config"`: true, - `Include = "~/.config/devbox/ssh/config"`: true, - `Include=~/.config/devbox/ssh/config`: true, - "Include\t~/.config/devbox/ssh/config": true, - "\tInclude ~/.config/devbox/ssh/config": true, - ` Include ~/.config/devbox/ssh/config`: true, - - `# Include ~/.config/devbox/ssh/config`: false, - `Include ~/.config/blah # ~/.config/devbox/ssh/config`: false, - `Include`: false, - `Include ~/.config/blah`: false, - `Include~/.config/devbox/ssh/config`: false, - `IdentityFile ~/.config/devbox/ssh/config/keys/mykey`: false, - `Hostname include devbox/ssh/config`: false, - } - for in, match := range tests { - t.Run(in, func(t *testing.T) { - got := reDevboxInclude.MatchString(in) - if got != match { - t.Errorf("got wrong match for %q\ngot match = %t, want %t", in, got, match) - } - }) - } -} - -func TestHostOrMatchRegex(t *testing.T) { - tests := map[string]bool{ - `Host *.devbox.sh`: true, - ` Host *.devbox.sh`: true, - "\tHost *.devbox.sh": true, - `Match all`: true, - ` Match all`: true, - "\tMatch all": true, - - "Host": false, - "Hostname devbox.sh": false, - `# Host *.devbox.sh`: true, - `# Match all`: true, - } - for in, match := range tests { - t.Run(in, func(t *testing.T) { - got := reHostOrMatch.MatchString(in) - if got != match { - t.Errorf("got wrong match for %q\ngot match = %t, want %t", in, got, match) - } - }) - } -} - -//go:embed testdata/devbox-ssh-config.golden -var goldenDevboxSSHConfig []byte - -func TestSetupDevbox(t *testing.T) { - want := fstest.MapFS{ - ".config": &fstest.MapFile{Mode: fs.ModeDir | 0o755}, - ".config/devbox": &fstest.MapFile{Mode: fs.ModeDir | 0o755}, - ".config/devbox/ssh": &fstest.MapFile{Mode: fs.ModeDir | 0o700}, - ".config/devbox/ssh/config": &fstest.MapFile{ - Data: goldenDevboxSSHConfig, - Mode: 0o644, - }, - ".config/devbox/ssh/known_hosts": &fstest.MapFile{ - Data: sshKnownHosts, - Mode: 0o644, - }, - ".config/devbox/ssh/sockets": &fstest.MapFile{Mode: fs.ModeDir | 0o700}, - - ".ssh": &fstest.MapFile{Mode: fs.ModeDir | 0o700}, - ".ssh/config": &fstest.MapFile{ - Data: []byte("Include \"$HOME/.config/devbox/ssh/config\"\n"), - Mode: 0o644, - }, - } - - t.Run("NoConfigs", func(t *testing.T) { - in := fstest.MapFS{} - workdir := fsToDir(t, in) - t.Setenv(envir.Home, workdir) - if err := SetupDevbox(); err != nil { - t.Error("got SetupDevbox() error:", err) - } - got := os.DirFS(workdir) - fsEqual(t, got, want) - }) - t.Run("ExistingSSHConfig", func(t *testing.T) { - existingSSHConfig := []byte("Host example.com\n\tUser example\n\tPort 1234\n") - input := fstest.MapFS{ - ".ssh": &fstest.MapFile{Mode: fs.ModeDir | 0o700}, - ".ssh/config": &fstest.MapFile{ - Data: existingSSHConfig, - Mode: 0o644, - }, - } - // Temporarily change the desired ~/.ssh/config so it contains - // the initial contents of the input ~/.ssh/config. - originalWantConfig := want[".ssh/config"] - defer func() { want[".ssh/config"] = originalWantConfig }() - want[".ssh/config"] = &fstest.MapFile{ - Data: append( - // fsEqual will expand $HOME so this becomes an absolute path. - []byte("Include \"$HOME/.config/devbox/ssh/config\"\n"), - existingSSHConfig..., - ), - Mode: 0o644, - } - - workdir := fsToDir(t, input) - t.Setenv(envir.Home, workdir) - if err := SetupDevbox(); err != nil { - t.Error("got SetupDevbox() error:", err) - } - got := os.DirFS(workdir) - fsEqual(t, got, want) - }) - t.Run("AlreadySetup", func(t *testing.T) { - in := want - workdir := fsToDir(t, in) - t.Setenv(envir.Home, workdir) - if err := SetupDevbox(); err != nil { - t.Error("got SetupDevbox() error:", err) - } - got := os.DirFS(workdir) - fsEqual(t, got, want) - }) -} - -//go:embed testdata/devbox-ssh-debug-config.golden -var goldenDevboxSSHDebugConfig []byte - -func TestSetupInsecureDebug(t *testing.T) { - wantAddr := "127.0.0.1:2222" - want := fstest.MapFS{ - ".config": &fstest.MapFile{Mode: fs.ModeDir | 0o755}, - ".config/devbox": &fstest.MapFile{Mode: fs.ModeDir | 0o755}, - ".config/devbox/ssh": &fstest.MapFile{Mode: fs.ModeDir | 0o700}, - ".config/devbox/ssh/config": &fstest.MapFile{ - Data: goldenDevboxSSHDebugConfig, - Mode: 0o644, - }, - ".config/devbox/ssh/known_hosts": &fstest.MapFile{ - Data: sshKnownHosts, - Mode: 0o644, - }, - ".config/devbox/ssh/known_hosts_debug": &fstest.MapFile{Mode: 0o644}, - ".config/devbox/ssh/sockets": &fstest.MapFile{Mode: fs.ModeDir | 0o700}, - - ".ssh": &fstest.MapFile{Mode: fs.ModeDir | 0o700}, - ".ssh/config": &fstest.MapFile{ - Data: []byte("Include \"$HOME/.config/devbox/ssh/config\"\n"), - Mode: 0o644, - }, - } - - t.Run("NoConfigs", func(t *testing.T) { - in := fstest.MapFS{} - workdir := fsToDir(t, in) - t.Setenv(envir.Home, workdir) - if err := SetupInsecureDebug(wantAddr); err != nil { - t.Errorf("got SetupInsecureDebug(%q) error: %v", wantAddr, err) - } - got := os.DirFS(workdir) - fsEqual(t, got, want) - }) - t.Run("ChangeHost", func(t *testing.T) { - input := fstest.MapFS{} - for k, v := range want { - input[k] = v - } - - // Set the initial config to have a debug host = 127.0.0.2 so we - // can check that it gets changed back to 127.0.0.1. - input[".config/devbox/ssh/config"] = &fstest.MapFile{ - Data: bytes.ReplaceAll(goldenDevboxSSHDebugConfig, []byte("127.0.0.1"), []byte("127.0.0.2")), - Mode: 0o644, - } - - // Put something in known_hosts_debug so we can check that it - // gets cleared out. - input[".config/devbox/ssh/known_hosts_debug"] = &fstest.MapFile{ - Data: []byte("[127.0.0.1]:2222 ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAPY1ms2jt+QPvhq89J8KF7rfTCFUi6X6Ik4O9EIAT/c\n"), - Mode: 0o644, - } - - workdir := fsToDir(t, input) - t.Setenv(envir.Home, workdir) - if err := SetupInsecureDebug(wantAddr); err != nil { - t.Errorf("got SetupInsecureDebug(%q) error: %v", wantAddr, err) - } - got := os.DirFS(workdir) - fsEqual(t, got, want) - }) -} - -//go:embed testdata/test.vm.devbox-vms.internal.golden -var goldenVMKey []byte - -func TestAddVMKey(t *testing.T) { - host := "test.vm.devbox-vms.internal" - input := fstest.MapFS{} - want := fstest.MapFS{ - ".config": &fstest.MapFile{Mode: fs.ModeDir | 0o755}, - ".config/devbox": &fstest.MapFile{Mode: fs.ModeDir | 0o755}, - ".config/devbox/ssh": &fstest.MapFile{Mode: fs.ModeDir | 0o700}, - ".config/devbox/ssh/keys": &fstest.MapFile{Mode: fs.ModeDir | 0o700}, - - ".config/devbox/ssh/keys/" + host: &fstest.MapFile{ - Data: goldenVMKey, - Mode: 0o600, - }, - } - - workdir := fsToDir(t, input) - t.Setenv(envir.Home, workdir) - if err := AddVMKey(host, string(goldenVMKey)); err != nil { - t.Error("got AddKey(host, key) error:", err) - } - got := os.DirFS(workdir) - fsEqual(t, got, want) -} - -// fsEqual checks if the contents of two file systems are the same. Two file -// systems are equal if their path hierarchies are the same and the file at -// each path passes fsPathsEqual. It ignores the mode of the root directory of -// each file system. -// -// fsEqual will report as many equality errors as possible by continuing to walk -// the tree after a file comparison fails. -func fsEqual(t *testing.T, got, want fs.FS) { - t.Helper() - - checked := map[string]bool{} - err := fs.WalkDir(got, ".", func(path string, _ fs.DirEntry, err error) error { - if err != nil { - return err - } - if path == "." { - return nil - } - checked[path] = true - fsPathsEqual(t, got, want, path) - return nil - }) - if err != nil { - t.Fatal("got error checking if file systems are equal:", err) - } - - err = fs.WalkDir(want, ".", func(path string, _ fs.DirEntry, err error) error { - if err != nil { - return err - } - if path == "." || checked[path] { - return nil - } - fsPathsEqual(t, got, want, path) - return nil - }) - if err != nil { - t.Fatal("got error checking if file systems are equal:", err) - } -} - -// fsPathsEqual checks if the file at a path in two file systems is the same. -// Two file paths are equal if: -// -// - their file contents are the same after calling os.ExpandEnv on each one. -// - their file mode dir bits are the same. -// - their file permission mode bits are the same. -// -// It does not consider any other file info such as ModTime or other file mode -// bits. -// -// Expanding environment variables in files makes it easier for tests to define -// golden files with dynamic content. For example, a test can create an -// fstest.MapFile with the data {"name": "$USER"} and compare it to some test -// results to make sure there's a JSON file containing the current username. -func fsPathsEqual(t *testing.T, gotFS, wantFS fs.FS, path string) { - t.Helper() - - gotInfo, err := fs.Stat(gotFS, path) - if errors.Is(err, fs.ErrNotExist) { - t.Errorf("got a missing file at %q", path) - return - } - if err != nil { - t.Errorf("got fs.Stat(gotFS, %q) error: %v", path, err) - } - wantInfo, err := fs.Stat(wantFS, path) - if errors.Is(err, fs.ErrNotExist) { - t.Errorf("got an extra file at %q", path) - return - } - if err != nil { - t.Errorf("got fs.Stat(wantFS, %q) error: %v", path, err) - } - - // Bail early to avoid nil pointer panics. - if gotInfo == nil || wantInfo == nil { - return - } - if got, want := gotInfo.Mode().Perm(), wantInfo.Mode().Perm(); got != want { - t.Errorf("got %q permissions %s, want %s", path, got, want) - } - if gotInfo.IsDir() != wantInfo.IsDir() { - gotType, wantType := "file", "file" - if gotInfo.IsDir() { - gotType = "directory" - } - if wantInfo.IsDir() { - wantType = "directory" - } - t.Errorf("got a %s at path %q, want a %s", gotType, path, wantType) - } - - // No need to compare file contents if either path is a directory. - if gotInfo.IsDir() || wantInfo.IsDir() { - return - } - - gotBytes, err := fs.ReadFile(gotFS, path) - if err != nil { - t.Errorf("got fs.ReadFile(gotFS, %q) error: %v", path, err) - } - wantBytes, err := fs.ReadFile(wantFS, path) - if err != nil { - t.Errorf("got fs.ReadFile(wantFS, %q) error: %v", path, err) - } - diff := cmp.Diff(os.ExpandEnv(string(wantBytes)), os.ExpandEnv(string(gotBytes))) - if diff != "" { - t.Errorf("got wrong file contents at %q (-want +got):\n%s", path, diff) - } -} - -// fsToDir writes a file system to a local temp directory. It replicates each -// file's contents and permissions, but ignores any other file info. If the -// root of the file system returns [fs.ErrNotExist], then fsToDir returns an -// empty temp directory. -func fsToDir(t *testing.T, fsys fs.FS) (dir string) { - t.Helper() - - dir = t.TempDir() - err := fs.WalkDir(fsys, ".", func(path string, entry fs.DirEntry, err error) error { - if path == "." { - // Just return an empty directory if the input is also - // empty. - if err == nil || errors.Is(err, fs.ErrNotExist) { - return nil - } - return err - } - - info, err := fs.Stat(fsys, path) - if err != nil { - t.Error("got error writing fs to dir:", err) - return nil - } - if info.Mode().Perm() == 0 { - // It's impossible to create a file that you can't write - // to. - t.Fatalf("got error writing fs to dir: path %q has empty permissions", path) - } - if entry.IsDir() { - if err := os.Mkdir(filepath.Join(dir, path), info.Mode().Perm()); err != nil { - t.Error("got error writing fs to dir:", err) - } - return nil - } - data, err := fs.ReadFile(fsys, path) - if err != nil { - t.Error("got error writing fs to dir:", err) - return nil - } - if err := os.WriteFile(filepath.Join(dir, path), data, info.Mode().Perm()); err != nil { - t.Error("got error writing fs to dir:", err) - } - return nil - }) - if err != nil { - t.Fatal("got error writing fs to dir:", err) - } - return dir -} diff --git a/internal/cloud/openssh/known_hosts b/internal/cloud/openssh/known_hosts deleted file mode 100644 index 01bb2773b60..00000000000 --- a/internal/cloud/openssh/known_hosts +++ /dev/null @@ -1,2 +0,0 @@ -gateway.devbox.sh ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGe3cR+Qu+yWlsn9nRYQQ5QYd9DY0yntrKJRyrHrYUWM -gateway.dev.devbox.sh ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAID4084z9HuI5zmjDsnAH2gyV+xWhYiWvOJ8JDx6btPv4 diff --git a/internal/cloud/openssh/sshconfig.tmpl b/internal/cloud/openssh/sshconfig.tmpl deleted file mode 100644 index 44715bcedee..00000000000 --- a/internal/cloud/openssh/sshconfig.tmpl +++ /dev/null @@ -1,34 +0,0 @@ -# Autogenerated by devbox. Do not modify manually. -# ConfigVersion: {{ .ConfigVersion }} - -Host proxy.devbox.sh - StrictHostKeyChecking no - UserKnownHostsFile "{{ .ConfigDir }}/known_hosts" - -Host gateway.devbox.sh gateway.dev.devbox.sh - HostKeyAlgorithms ssh-ed25519 - StrictHostKeyChecking yes - UserKnownHostsFile "{{ .ConfigDir }}/known_hosts" - -Host *.devbox-vms.internal - Port 2222 - ProxyJump proxy@proxy.devbox.sh - IdentityFile "{{ .ConfigDir }}/keys/%h" - PreferredAuthentications publickey - StrictHostKeyChecking no - UserKnownHostsFile "{{ .ConfigDir }}/known_hosts" - ServerAliveInterval 50 - ServerAliveCountMax 3 - ControlMaster auto - ControlPath "{{ .ConfigDir }}/sockets/%h" - ControlPersist 300 - -{{- if .DebugGateway.Host }} - -Host gateway.devbox-debug {{ .DebugGateway.Host }} - Hostname {{ .DebugGateway.Host }} - Port {{ .DebugGateway.Port }} - StrictHostKeyChecking no - UserKnownHostsFile "{{ .ConfigDir }}/known_hosts_debug" - -{{- end }} diff --git a/internal/cloud/openssh/sshshim/command.go b/internal/cloud/openssh/sshshim/command.go deleted file mode 100644 index db46bf4cc4a..00000000000 --- a/internal/cloud/openssh/sshshim/command.go +++ /dev/null @@ -1,49 +0,0 @@ -// Copyright 2024 Jetify Inc. and contributors. All rights reserved. -// Use of this source code is governed by the license in the LICENSE file. - -package sshshim - -import ( - "context" - "fmt" - "log/slog" - "os" - - "go.jetpack.io/devbox/internal/debug" - "go.jetpack.io/devbox/internal/telemetry" -) - -func Execute(ctx context.Context, args []string) int { - defer debug.Recover() - telemetry.Start() - defer telemetry.Stop() - - if err := execute(ctx, args); err != nil { - telemetry.Error(err, telemetry.Metadata{}) - return 1 - } - return 0 -} - -func execute(ctx context.Context, args []string) error { - EnableDebug() // Always enable for now. - slog.Debug("sshshim.execute", "args", args) - - alive, err := EnsureLiveVMOrTerminateMutagenSessions(ctx, args[1:]) - if err != nil { - slog.Error("ensureLiveVMOrTerminateMutagenSessions error", "err", err) - fmt.Fprintf(os.Stderr, "%v", err) - return err - } - if !alive { - return nil - } - - if err := InvokeSSHOrSCPCommand(args); err != nil { - slog.Debug("InvokeSSHorSCPCommand error", "err", err) - fmt.Fprintf(os.Stderr, "%v", err) - return err - } - - return nil -} diff --git a/internal/cloud/openssh/sshshim/generate.go b/internal/cloud/openssh/sshshim/generate.go deleted file mode 100644 index 8a9954a1d4e..00000000000 --- a/internal/cloud/openssh/sshshim/generate.go +++ /dev/null @@ -1,55 +0,0 @@ -// Copyright 2024 Jetify Inc. and contributors. All rights reserved. -// Use of this source code is governed by the license in the LICENSE file. - -package sshshim - -import ( - "io/fs" - "os" - "path/filepath" - - "github.com/pkg/errors" - - "go.jetpack.io/devbox/internal/cloud/mutagenbox" - "go.jetpack.io/devbox/internal/cloud/openssh" -) - -// Setup creates the ssh and scp symlinks -func Setup() error { - shimDir, err := mutagenbox.ShimDir() - if err != nil { - return errors.WithStack(err) - } - - if err := openssh.EnsureDirExists(shimDir, 0o744, true /*chmod*/); err != nil { - return err - } - - devboxExecutablePath, err := os.Executable() - if err != nil { - return errors.WithStack(err) - } - - // create ssh symlink - sshSymlink := filepath.Join(shimDir, "ssh") - if err := makeSymlink(sshSymlink, devboxExecutablePath); err != nil { - return errors.WithStack(err) - } - - // create scp symlink - scpSymlink := filepath.Join(shimDir, "scp") - return errors.WithStack(makeSymlink(scpSymlink, devboxExecutablePath)) -} - -func makeSymlink(from, target string) error { - err := os.Remove(from) - if err != nil && !errors.Is(err, fs.ErrNotExist) { - return errors.WithStack(err) - } - - err = os.Symlink(target, from) - if errors.Is(err, fs.ErrExist) { - err = nil - } - return errors.WithStack(err) -} diff --git a/internal/cloud/openssh/sshshim/invoke.go b/internal/cloud/openssh/sshshim/invoke.go deleted file mode 100644 index d733e1b8c5e..00000000000 --- a/internal/cloud/openssh/sshshim/invoke.go +++ /dev/null @@ -1,48 +0,0 @@ -// Copyright 2024 Jetify Inc. and contributors. All rights reserved. -// Use of this source code is governed by the license in the LICENSE file. - -package sshshim - -import ( - "log/slog" - "os" - "os/exec" - "strings" - "syscall" - - "github.com/pkg/errors" -) - -func InvokeSSHOrSCPCommand(args []string) error { - if !strings.HasSuffix(args[0], "ssh") && !strings.HasSuffix(args[0], "scp") { - return errors.Errorf("received %s for args[0], but expected ssh or scp", args[0]) - } - - executableName := "ssh" - if strings.HasSuffix(args[0], "scp") { - executableName = "scp" - } - - // We need to look for ssh or scp in PATH. If we directly call "ssh", for example, - // then we recursively loop into calling the ssh-named symlink that points to devbox. - executablePath, err := exec.LookPath(executableName) - if err != nil { - return errors.WithStack(err) - } - - // We set executablePath to the first argument in `args` because: - // - // https://man7.org/linux/man-pages/man2/execve.2.html - // argv is an array of pointers to strings passed to the new program - // as its command-line arguments. By convention, the first of these - // strings (i.e., argv[0]) should contain the filename associated - // with the file being executed. - args[0] = executablePath - slog.Debug("invoking %s with args: %v", args[0], args) - - // Choose syscall.Exec instead of exec.Cmd so that we preserve the exit code - // and environment, and the current process is replaced by ssh. - // Without this, we'd see errors during mutagen sync: the mutagen binary - // would fail to be copied to Beta, the remote machine. - return errors.WithStack(syscall.Exec(executablePath, args, os.Environ())) -} diff --git a/internal/cloud/openssh/sshshim/logger.go b/internal/cloud/openssh/sshshim/logger.go deleted file mode 100644 index f4d9f25d410..00000000000 --- a/internal/cloud/openssh/sshshim/logger.go +++ /dev/null @@ -1,50 +0,0 @@ -// Copyright 2024 Jetify Inc. and contributors. All rights reserved. -// Use of this source code is governed by the license in the LICENSE file. - -package sshshim - -// The sshshim is invoked by mutagen daemon, so we log errors to a file which -// we can inspect. - -import ( - "fmt" - "io" - "log/slog" - "os" - "path/filepath" - - "github.com/pkg/errors" - "go.jetpack.io/devbox/internal/cloud/mutagenbox" - "go.jetpack.io/devbox/internal/debug" - "gopkg.in/natefinch/lumberjack.v2" -) - -const ( - logFileName = "logs.txt" -) - -func EnableDebug() { - if w, err := logFileWriter(); err == nil { - debug.SetOutput(w) - } else { - fmt.Fprintf(os.Stderr, "failed to init ssh log file: %s", err) - } - debug.Enable() - slog.Debug("started sshshim\n") -} - -// logFile captures output for logging and when there is a failure -func logFileWriter() (io.Writer, error) { - dirPath, err := mutagenbox.ShimDir() - if err != nil { - return nil, errors.WithStack(err) - } - - return &lumberjack.Logger{ - Filename: filepath.Join(dirPath, logFileName), - MaxSize: 2, // megabytes - MaxBackups: 2, - MaxAge: 28, // days - Compress: true, // disabled by default - }, nil -} diff --git a/internal/cloud/openssh/sshshim/mutagen.go b/internal/cloud/openssh/sshshim/mutagen.go deleted file mode 100644 index 5338f736b24..00000000000 --- a/internal/cloud/openssh/sshshim/mutagen.go +++ /dev/null @@ -1,119 +0,0 @@ -// Copyright 2024 Jetify Inc. and contributors. All rights reserved. -// Use of this source code is governed by the license in the LICENSE file. - -package sshshim - -import ( - "bytes" - "context" - "log/slog" - "os/exec" - "strings" - "time" - - "github.com/pkg/errors" - - "go.jetpack.io/devbox/internal/cloud/mutagenbox" -) - -// EnsureLiveVMOrTerminateMutagenSessions returns true if a liveVM is found, OR sshArgs were connecting to a server that is not a devbox-VM. -// EnsureLiveVMOrTerminateMutagenSessions returns false iff the sshArgs were connecting to a devbox VM AND a deadVM is found. -func EnsureLiveVMOrTerminateMutagenSessions(ctx context.Context, sshArgs []string) (bool, error) { - vmAddr := vmAddressIfAny(sshArgs) - - slog.Debug("found vm address", "addr", vmAddr) - if vmAddr == "" { - // We support the no Vm scenario, in case mutagen ssh-es into another server - // TODO savil. Revisit the no VM scenario if we can control the mutagen daemon for devbox-only - // syncing via MUTAGEN_DATA_DIRECTORY. - return true, nil - } - - isActive, err := checkActiveVMWithRetries(ctx, vmAddr) - if err != nil { - return false, errors.WithStack(err) - } - if !isActive { - slog.Debug("terminating vm mutagen session", "addr", vmAddr) - // If no vm is active, then we should terminate the running mutagen sessions - return false, terminateMutagenSessions(vmAddr) - } - return true, nil -} - -func terminateMutagenSessions(vmAddr string) error { - username, hostname, found := strings.Cut(vmAddr, "@") - if !found { - hostname = username - } - machineID, _, found := strings.Cut(hostname, ".") - if !found { - return errors.Errorf( - "expected to find a period (.) in hostname (%s), but did not. "+ - "For completeness, VmAddr is %s", hostname, vmAddr) - } - - if err := mutagenbox.TerminateSessionsForMachine(machineID, nil /*env*/); err != nil { - return err - } - - return mutagenbox.ForwardTerminateByHost(hostname) -} - -func checkActiveVMWithRetries(ctx context.Context, vmAddr string) (bool, error) { - var finalErr error - - // Try 3 times: - for num := 0; num < 3; num++ { - isActive, err := checkActiveVM(ctx, vmAddr) - if err == nil && isActive { - // found an active VM - return true, nil - } - finalErr = err - time.Sleep(10 * time.Second) - slog.Debug("failed to find active vm", "attempt", num, "addr", vmAddr) - } - return false, finalErr -} - -func checkActiveVM(ctx context.Context, vmAddr string) (bool, error) { - ctx, cancel := context.WithTimeout(ctx, time.Minute*2) - defer cancel() - cmd := exec.CommandContext(ctx, "ssh", vmAddr, "echo 'alive'") - - var bufErr, bufOut bytes.Buffer - cmd.Stderr = &bufErr - cmd.Stdout = &bufOut - - err := cmd.Run() - if err != nil { - if e := (&exec.ExitError{}); errors.As(err, &e) && e.ExitCode() == 255 { - slog.Debug("checkActiveVM: No active VM. returning false for exit status 255") - return false, nil - } - // For now, any error is deemed to indicate a VM that is no longer running. - // We can tighten this by listening for the specific exit error code (255) - slog.Debug("Error checking for Active VM: %s. Stdout: %s, Stderr: %s, cmd.Run err: %s\n", - vmAddr, - bufOut.String(), - bufErr.String(), - err, - ) - return false, errors.WithStack(err) - } - return true, nil -} - -// vmAddressIfAny will seek to find the devbox-vm hostname if it exists -// in the sshArgs. If not, it returns an empty string. -func vmAddressIfAny(sshArgs []string) string { - const devboxVMAddressSuffix = "devbox-vms.internal" - for _, sshArg := range sshArgs { - if strings.HasSuffix(sshArg, devboxVMAddressSuffix) { - return sshArg - } - } - slog.Debug("did not find vm address in ssh args", "args", sshArgs) - return "" -} diff --git a/internal/cloud/openssh/testdata/devbox-ssh-config.golden b/internal/cloud/openssh/testdata/devbox-ssh-config.golden deleted file mode 100644 index f2ca9ddeba6..00000000000 --- a/internal/cloud/openssh/testdata/devbox-ssh-config.golden +++ /dev/null @@ -1,24 +0,0 @@ -# Autogenerated by devbox. Do not modify manually. -# ConfigVersion: 0.0.1 - -Host proxy.devbox.sh - StrictHostKeyChecking no - UserKnownHostsFile "$HOME/.config/devbox/ssh/known_hosts" - -Host gateway.devbox.sh gateway.dev.devbox.sh - HostKeyAlgorithms ssh-ed25519 - StrictHostKeyChecking yes - UserKnownHostsFile "$HOME/.config/devbox/ssh/known_hosts" - -Host *.devbox-vms.internal - Port 2222 - ProxyJump proxy@proxy.devbox.sh - IdentityFile "$HOME/.config/devbox/ssh/keys/%h" - PreferredAuthentications publickey - StrictHostKeyChecking no - UserKnownHostsFile "$HOME/.config/devbox/ssh/known_hosts" - ServerAliveInterval 50 - ServerAliveCountMax 3 - ControlMaster auto - ControlPath "$HOME/.config/devbox/ssh/sockets/%h" - ControlPersist 300 diff --git a/internal/cloud/openssh/testdata/devbox-ssh-debug-config.golden b/internal/cloud/openssh/testdata/devbox-ssh-debug-config.golden deleted file mode 100644 index ee21a52210b..00000000000 --- a/internal/cloud/openssh/testdata/devbox-ssh-debug-config.golden +++ /dev/null @@ -1,30 +0,0 @@ -# Autogenerated by devbox. Do not modify manually. -# ConfigVersion: 0.0.1 - -Host proxy.devbox.sh - StrictHostKeyChecking no - UserKnownHostsFile "$HOME/.config/devbox/ssh/known_hosts" - -Host gateway.devbox.sh gateway.dev.devbox.sh - HostKeyAlgorithms ssh-ed25519 - StrictHostKeyChecking yes - UserKnownHostsFile "$HOME/.config/devbox/ssh/known_hosts" - -Host *.devbox-vms.internal - Port 2222 - ProxyJump proxy@proxy.devbox.sh - IdentityFile "$HOME/.config/devbox/ssh/keys/%h" - PreferredAuthentications publickey - StrictHostKeyChecking no - UserKnownHostsFile "$HOME/.config/devbox/ssh/known_hosts" - ServerAliveInterval 50 - ServerAliveCountMax 3 - ControlMaster auto - ControlPath "$HOME/.config/devbox/ssh/sockets/%h" - ControlPersist 300 - -Host gateway.devbox-debug 127.0.0.1 - Hostname 127.0.0.1 - Port 2222 - StrictHostKeyChecking no - UserKnownHostsFile "$HOME/.config/devbox/ssh/known_hosts_debug" diff --git a/internal/cloud/openssh/testdata/test.vm.devbox-vms.internal.golden b/internal/cloud/openssh/testdata/test.vm.devbox-vms.internal.golden deleted file mode 100644 index 729c70aba4e..00000000000 --- a/internal/cloud/openssh/testdata/test.vm.devbox-vms.internal.golden +++ /dev/null @@ -1,8 +0,0 @@ ------BEGIN OPENSSH PRIVATE KEY----- -b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW -QyNTUxOQAAACAFBvrkKm1Zrwbpp7DAmTWj7i+S+cjGhAwm++fELg1OpwAAAKjD4XIZw+Fy -GQAAAAtzc2gtZWQyNTUxOQAAACAFBvrkKm1Zrwbpp7DAmTWj7i+S+cjGhAwm++fELg1Opw -AAAEAcvFwROtvcGVsdSg73Y+znyO9F6LFRxhWa7UJdGcjGzwUG+uQqbVmvBumnsMCZNaPu -L5L5yMaEDCb758QuDU6nAAAAH2djdXJ0aXNAR3JlZ3MtTWFjQm9vay1Qcm8ubG9jYWwBAg -MEBQY= ------END OPENSSH PRIVATE KEY----- diff --git a/internal/cloud/openssh/username.go b/internal/cloud/openssh/username.go deleted file mode 100644 index 4e1285fe056..00000000000 --- a/internal/cloud/openssh/username.go +++ /dev/null @@ -1,46 +0,0 @@ -// Copyright 2024 Jetify Inc. and contributors. All rights reserved. -// Use of this source code is governed by the license in the LICENSE file. - -package openssh - -import ( - "os" - "path/filepath" - - "github.com/pkg/errors" - "go.jetpack.io/devbox/internal/fileutil" -) - -func GithubUsernameFromLocalFile() (string, error) { - filePath, err := usernameFilePath() - if err != nil { - return "", err - } - if !fileutil.Exists(filePath) { - return "", nil - } - - username, err := os.ReadFile(filePath) - if err != nil { - return "", errors.WithStack(err) - } - return string(username), nil -} - -func SaveGithubUsernameToLocalFile(username string) error { - filePath, err := usernameFilePath() - if err != nil { - return errors.WithStack(err) - } - - return errors.WithStack(os.WriteFile(filePath, []byte(username), 0o600)) -} - -func usernameFilePath() (string, error) { - sshDir, err := devboxSSHDir() - if err != nil { - return "", errors.WithStack(err) - } - - return filepath.Join(sshDir, "github_username"), nil -} diff --git a/internal/cloud/user.go b/internal/cloud/user.go deleted file mode 100644 index 2064f8f3372..00000000000 --- a/internal/cloud/user.go +++ /dev/null @@ -1,50 +0,0 @@ -// Copyright 2024 Jetify Inc. and contributors. All rights reserved. -// Use of this source code is governed by the license in the LICENSE file. - -package cloud - -import ( - "bytes" - "log/slog" - "os/exec" - "regexp" - - "github.com/pkg/errors" -) - -var githubSSHRegexp = regexp.MustCompile("Hi (.+)! You've successfully authenticated, " + - "but GitHub does not provide shell access") - -// queryGithubUsername attempts to make an ssh connection to github, which replies -// with a friendly rejection message that contains the user's username (if they have github -// credentials set up correctly). We parse the username from the error message. -func queryGithubUsername() (string, error) { - cmd := exec.Command("ssh", "-T", "-o", "NumberOfPasswordPrompts=0", "git@github.com") - var bufOut, bufErr bytes.Buffer - cmd.Stdin = nil - cmd.Stdout = &bufOut - cmd.Stderr = &bufErr - err := cmd.Run() - if err != nil { - if e := (&exec.ExitError{}); errors.As(err, &e) && e.ExitCode() == 1 { - // This is the Happy case, and we can parse out the error message - slog.Debug("received expected (this is good) error with exit code 1", "cmd", cmd, "stderr", bufErr.String()) - return parseUsernameFromErrorMessage(bufErr.String()), nil - } - // This is the sad case, and we should let the caller figure out how to proceed with the user - slog.Error("error from command", "cmd", cmd, "err", err, "stdout", bufOut.String(), "stderr", bufErr.String()) - return "", errors.WithStack(err) - } - - return "", nil -} - -func parseUsernameFromErrorMessage(errorMessage string) string { - matchedUsernames := githubSSHRegexp.FindSubmatch([]byte(errorMessage)) - if len(matchedUsernames) < 2 { - slog.Debug("did not find a username from github", "github_msg", errorMessage) - return "" - } - slog.Debug("matched username from github", "user", matchedUsernames[1]) - return string(matchedUsernames[1]) -} diff --git a/internal/cloud/user_test.go b/internal/cloud/user_test.go deleted file mode 100644 index 1a010c03941..00000000000 --- a/internal/cloud/user_test.go +++ /dev/null @@ -1,40 +0,0 @@ -// Copyright 2024 Jetify Inc. and contributors. All rights reserved. -// Use of this source code is governed by the license in the LICENSE file. - -package cloud - -import ( - "testing" -) - -func TestParseUsernameFromErrorMessage(t *testing.T) { - testCases := []struct { - name string - errMessage string - username string - }{ - { - "success_case", - "Hi myDearUsername! You've successfully authenticated, but GitHub does not provide shell access.", - "myDearUsername", - }, - { - // NOTE this case won't actually occur because parseUsernameFromErrorMessage - // is only run for ExitCode == 1, but pub_key_denied is a local ssh error with ExitCode == 255 - // - // Adding the test case to exercise the scenario where the error message doesn't match the regexp. - "pub_key_denied_case", - "public key denied", - "", - }, - } - - for _, testCase := range testCases { - t.Run(testCase.name, func(t *testing.T) { - result := parseUsernameFromErrorMessage(testCase.errMessage) - if result != testCase.username { - t.Errorf("expected %s username but got %s username", testCase.username, result) - } - }) - } -} diff --git a/internal/devbox/providers/doc.go b/internal/devbox/providers/doc.go deleted file mode 100644 index 91e927882a5..00000000000 --- a/internal/devbox/providers/doc.go +++ /dev/null @@ -1,6 +0,0 @@ -// providers encapsulates data and/or logic that can affect devbox behavior. -// What a provider does can be influenced by the environment or external services. -// The goal is to centralize this logic and avoid conditionals in core devbox code. -// In the future we should allow dynamic providers as well which can help -// customize devbox behavior. -package providers diff --git a/internal/nix/nixstore/fs.go b/internal/nix/nixstore/fs.go deleted file mode 100644 index 5b6fbb6728b..00000000000 --- a/internal/nix/nixstore/fs.go +++ /dev/null @@ -1,88 +0,0 @@ -// Copyright 2024 Jetify Inc. and contributors. All rights reserved. -// Use of this source code is governed by the license in the LICENSE file. - -package nixstore - -import ( - "errors" - "fmt" - "io/fs" - "os" - "path/filepath" -) - -// readLinkFS is an [os.DirFS] that supports reading symlinks. It satisfies -// the interface discussed in the [accepted Go proposal for fs.ReadLinkFS]. -// If differs from the proposed implementation in that it allows absolute -// symlinks by translating them to relative paths. -// -// [accepted Go proposal for fs.ReadLinkFS]: https://github.com/golang/go/issues/49580 -type readLinkFS struct { - fs.FS - dir string -} - -func newReadLinkFS(dir string) fs.FS { - return &readLinkFS{FS: os.DirFS(dir), dir: dir} -} - -func (fsys *readLinkFS) ReadLink(name string) (string, error) { - osName := filepath.Join(fsys.dir, filepath.FromSlash(name)) - dst, err := os.Readlink(osName) - if err != nil { - return "", err - } - if !filepath.IsAbs(dst) { - dst = filepath.Join(filepath.Dir(osName), dst) - } - if filepath.IsAbs(dst) { - dst, err = filepath.Rel(fsys.dir, dst) - if err != nil { - return "", fmt.Errorf("%s evaluates to a path outside of the root", name) - } - } - if !filepath.IsLocal(dst) { - return "", fmt.Errorf("%s evaluates to a path outside of the root", name) - } - return dst, nil -} - -// readLink returns the destination of a symbolic link. If the file system -// doesn't implement ReadLink, then it returns an error. It matches the -// interface discussed in the [accepted Go proposal for fs.ReadLink]. -// -// [accepted Go proposal for fs.ReadLink]: https://github.com/golang/go/issues/49580 -func readLink(fsys fs.FS, name string) (string, error) { - rlFS, ok := fsys.(interface{ ReadLink(string) (string, error) }) - if !ok { - return "", &fs.PathError{ - Op: "readlink", - Path: name, - Err: errors.New("not implemented"), - } - } - return rlFS.ReadLink(name) -} - -// readDirUnsorted acts identically to [fs.ReadDir] except that it skips -// sorting the directory entries when possible to save some time. -func readDirUnsorted(fsys fs.FS, path string) ([]fs.DirEntry, error) { - if fsys, ok := fsys.(fs.ReadDirFS); ok { - return fsys.ReadDir(path) - } - f, err := fsys.Open(".") - if err != nil { - return nil, err - } - defer f.Close() - - dir, ok := f.(fs.ReadDirFile) - if !ok { - return nil, &fs.PathError{ - Op: "readdir", - Path: path, - Err: errors.New("not implemented"), - } - } - return dir.ReadDir(-1) -} diff --git a/internal/nix/nixstore/indexpkgs.nix b/internal/nix/nixstore/indexpkgs.nix deleted file mode 100755 index 0c27547bce8..00000000000 --- a/internal/nix/nixstore/indexpkgs.nix +++ /dev/null @@ -1,191 +0,0 @@ -#!/usr/bin/env nix eval --read-only --show-trace --json --file - -/* indexpkgs.nix is an expression that starts with the top-level attributes in - nixpkgs and recursively walks the tree looking for derivations. The output is - an attribute set containing package information keyed by hash. - - This expression doesn't descend into attribute sets that are missing the - recurseForDerivations attribute. Derivations that fail to evaluate are - silently skipped. - - # Performance - - Evaluating all of nixpkgs is slow and consumes large amounts of memory. On a - fast machine this expression takes about 40s to execute and requires up to - 20 GiB of RAM. Be aware of any performance impacts your changes may have. - - # Debugging - - Put the following shebang at the top of this file and make it executable in - order to run it with `./indexpkgs.nix | jq .`: - - #!/usr/bin/env nix eval --read-only --show-trace --json --file -*/ - -with builtins; - -let - /* When changing this file, try to only use built-in Nix functions and avoid - any functions or constants in nixpkgs itself. Otherwise this expression - will fail when run against an older nixpkgs commit that doesn't have the - necessary dependencies. - */ - pkgs = import - (fetchTarball { - url = "https://github.com/nixos/nixpkgs/archive/{{ . }}.tar.gz"; - }) - # Comment out the fetchTarball above and uncomment the below function below - # to debug with a local clone of nixpkgs. - # - # (fetchGit { - # url = ""; - # rev = "3364b5b117f65fe1ce65a3cdd5612a078a3b31e3"; - # allRefs = true; - # ref = "master"; - # }) - { - config = { - # Always include unfree or broken packages in the index. Devbox can choose - # whether or not to show them in search results. - allowUnfree = true; - allowBroken = true; - }; - }; - - /* isDerivation returns true if x evaluates to a derivation. - - Type: - isDerivation :: Any -> Bool - */ - isDerivation = x: (x.type or null) == "derivation"; - - /* shouldRecurse returns true if walkNixpkgs should descend into x to look for - more derivations. It relies on a nixpkgs convention where a - recurseForDerivations attribute is set to true when a set may contain - child derivations. - - Type: - shouldRecurse :: Any -> Bool - */ - shouldRecurse = x: (tryEval (x.recurseForDerivations or false)).value; - - /* The following functions extract package information from various derivation - attributes. - */ - getName = drv: drv.pname or (parseDrvName drv.name).name; - getVersion = drv: splitVersion (toString (drv.version or (parseDrvName drv.name).version)); - getHomepage = drv: if isList drv then head drv else drv; - getLicense = drv: if isList drv then (head drv).spdxId else if isAttrs drv then drv.spdxId else drv; - getPkgInfo = drv: { - /* The name of the package without its version. - - Examples: - go - python3 - bash - */ - name = getName drv; - - /* The package version split into a list of its components. The version is - split using the splitVersion built-in function so that Devbox can do - ordered comparisons. - - Examples: - [ "1" "19" "3" ] # go 1.19.3 - [ "3" "11" "1" ] # python 3.11.1 - */ - version = getVersion drv; - - /* A list of attribute paths that point to this package. In other words, - they all point to the same derivation with the same hash. - - Examples: - [ "python3" "python310" "python310Packages.python" ... ] - [ "go" "go_1_19" ] - */ - paths = [ ]; - - # The remaining attributes are all optional. - ${if drv ? meta.mainProgram then "program" else null} = drv.meta.mainProgram; - ${if drv ? meta.description then "summary" else null} = drv.meta.description; - ${if drv ? meta.longDescription then "description" else null} = drv.meta.longDescription; - ${if drv ? meta.homepage then "homepage" else null} = getHomepage drv.meta.homepage; - ${if drv ? meta.license.spdxId then "license" else null} = getLicense drv.meta.license; - ${if drv ? meta.platforms then "platforms" else null} = drv.meta.platforms; - ${if drv ? meta.broken then "broken" else null} = drv.meta.broken; - ${if drv ? meta.insecure then "insecure" else null} = drv.meta.insecure; - }; - - /* appendAttrPath adds an attribute path to a package info. A single package - may have multiple paths pointing to it. For example, "python3" - and "python310" will resolve to the same derivation if Python 3.10 is the - default Python 3 interpreter. - - Type: - appendAttrPath :: AttrSet -> [ String ] -> AttrSet - */ - appendAttrPath = pkgInfo: attrPath: pkgInfo // { - paths = pkgInfo.paths ++ [ (concatStringsSep "." attrPath) ]; - }; - - /* walkNixpkgs starts at the top-level of nixpkgs and recursively walks its - attributes looking for derivations, building up allPkgs as it goes. It - returns an attribute set containing package info keyed by package hash. - - Each walkNixpkgs call enumerates the attributes in attrSet and attempts to - evaluate their values as a derivation. If the evaluation succeeds, it adds - the derivation's hash and package info to allPkgs. If the derivation's - hash is already in allPkgs, it appends the current attribute path to the - existing package info. Finally, it recursively calls itself on each - attribute value (even if the attribute wasn't a derivation) to look for - any derivations in nested attribute sets. - - Type: - walkNixpkgs :: AttrSet -> [ String ] -> AttrSet -> AttrSet - */ - walkNixpkgs = allPkgs: attrPath: attrSet: foldl' - (allPkgs: attrName: - let - attrValue = attrSet."${attrName}"; - attrValuePath = attrPath ++ [ attrName ]; - tryDerivationHash = tryEval ( - if isDerivation attrValue then - # unsafeDiscardStringContext allows the substring to be used as a - # key in an attribute set. Nix reports an error otherwise. - substring 0 32 (unsafeDiscardStringContext (baseNameOf attrValue.outPath)) - else - null - ); - derivationHash = if tryDerivationHash.success then tryDerivationHash.value else null; - pkgInfo = appendAttrPath (allPkgs.${derivationHash} or (getPkgInfo attrValue)) attrValuePath; - - # Rely on the behavior where attributes are automatically omitted from a - # set when their name is null. That makes this update a no-op when - # derivationHash failed to evaluate or wasn't a derivation. - updatedAllPkgs = allPkgs // { ${derivationHash} = pkgInfo; }; - in - ( - if shouldRecurse attrValue then - walkNixpkgs updatedAllPkgs attrValuePath attrValue - else - updatedAllPkgs - ) - ) - allPkgs - (attrNames attrSet); -in - - /* Keep the following things in mind if you're changing the JSON output: - - - Nix sorts JSON field names alphabetically. When renaming fields, make - sure that package count comes before the array of packages so that - Devbox can preallocate space. - - Keep the JSON as flat as possible to simplify the Go parsing code and - make debugging easier. - */ -rec { - count = length (attrNames packages); - system = currentSystem; - nix = nixVersion; - packages = walkNixpkgs { } [ ] pkgs; -} diff --git a/internal/nix/nixstore/nixstore.go b/internal/nix/nixstore/nixstore.go deleted file mode 100644 index 777e10316d3..00000000000 --- a/internal/nix/nixstore/nixstore.go +++ /dev/null @@ -1,261 +0,0 @@ -// Copyright 2024 Jetify Inc. and contributors. All rights reserved. -// Use of this source code is governed by the license in the LICENSE file. - -// Package nixstore queries and resolves Nix store packages. -package nixstore - -import ( - "errors" - "fmt" - "io/fs" - "path" - "strings" -) - -// Root is the top-level directory of a Nix store. It maintains an index of -// packages for fast lookup and dependency resolution. -type Root struct { - fs.FS - - // pkgs holds package information for every indexed package in the store. - // Other fields point into this slice, so it should only be appended to and - // not reordered. - pkgs []Package - - // pkgByHash indexes packages by their store hash. Its entries point to the - // packages in pkgs. - pkgByHash map[string]*Package - - // storeHashes contains just package hashes for use with depScanner. Each hash - // maps directly to the corresponding index in pkgs. - storeHashes [][]byte - - // depScanner scans through the files in a package, looking for references to - // other package hashes. - depScanner dependencyScanner -} - -// Local returns a local file system Nix store, which is typically /nix/store. -func Local(path string) (*Root, error) { - return &Root{FS: newReadLinkFS(path)}, nil -} - -// Package retrieves a package by its name within the store. The name must be -// the fully unique directory name following the pattern -. -func (r *Root) Package(name string) (*Package, error) { - cleaned := path.Clean(name) - if cleaned == "." { - return nil, errors.New("name is empty") - } - if strings.ContainsRune(cleaned, '/') { - return nil, errors.New("name contains a '/'") - } - if !fs.ValidPath(name) { - return nil, fmt.Errorf("invalid package name %q", name) - } - - pkg := r.pkgByHash[name[:32]] - if pkg == nil { - if err := r.buildIndex(); err != nil { - return nil, err - } - pkg = r.pkgByHash[name[:32]] - if pkg == nil { - return nil, fmt.Errorf("package not found: %s", name) - } - } - return pkg, r.resolveDeps(pkg) -} - -// indexPkg returns a [Package] with the given store name, adding it to the -// index if necessary. It assumes that the name is valid and in - -// format. -func (r *Root) indexPkg(name string) (*Package, error) { - hash := name[:32] - if pkg := r.pkgByHash[hash]; pkg != nil { - // We already have an instance of this package. - return pkg, nil - } - - // Add the package to end of the index slice and return a pointer to it. - i := len(r.pkgs) - r.pkgs = append(r.pkgs, Package{ - StoreName: name, - Hash: hash, - }) - r.storeHashes = append(r.storeHashes, []byte(hash)) - pkg := &r.pkgs[i] - - var err error - pkg.FS, err = fs.Sub(r, name) - if err != nil { - // Undo appending the new package before returning an error. - r.pkgs = r.pkgs[:i] - return nil, fmt.Errorf("unable to open package %s: %v", name, err) - } - r.pkgByHash[pkg.Hash] = pkg - return pkg, nil -} - -// buildIndex lists all of the files in the store root and indexes them by -// their hashes. This can be somewhat slow (1-2s) for a store with a lot of -// packages. -func (r *Root) buildIndex() error { - entries, err := readDirUnsorted(r, ".") - if err != nil { - return fmt.Errorf("unable to list Nix store root directory: %w", err) - } - - if r.pkgByHash == nil { - r.pkgByHash = make(map[string]*Package, len(entries)) - } - if cap(r.pkgs) < len(entries) { - // Make sure we'll have enough capacity for the entries to avoid allocations. - newCap := len(entries) + len(r.pkgs) - - pkgs := make([]Package, len(r.pkgs), newCap) - copy(pkgs, r.pkgs) - r.pkgs = pkgs - - hashes := make([][]byte, len(r.storeHashes), newCap) - copy(hashes, r.storeHashes) - r.storeHashes = hashes - } - -entries: - for _, entry := range entries { - // Skip hidden files or those that are too short to have a - // base-32 hash prefix. - name := entry.Name() - if len(name) < 32 || name[0] == '.' { - continue - } - - // Make sure the hash is valid. Nix hashes must be alphanumeric without the - // letters 'e', 'o', 't', or 'u'. - hash := name[:32] - for _, ch := range hash { - switch { - case '0' <= ch && ch <= '9': - case 'a' <= ch && ch <= 'z': - switch ch { - case 'e', 'o', 't', 'u': - continue entries - } - default: - continue entries - } - } - if _, err := r.indexPkg(name); err != nil { - return err - } - } - r.depScanner = newDependencyScanner(r.storeHashes) - return nil -} - -// resolveDeps populates the direct dependencies of pkg. -func (r *Root) resolveDeps(pkg *Package) error { - if pkg.DirectDependencies != nil { - // Already resolved. - return nil - } - - // Find dependencies by looking at every file in the package to see if it - // references another package's hash. - foundDeps := map[*Package]struct{}{} - err := fs.WalkDir(pkg, ".", func(entryPath string, entry fs.DirEntry, err error) error { - if err != nil { - return err - } - - // Scan the contents of regular files for references to other packages. - if entry.Type().IsRegular() { - f, err := pkg.Open(entryPath) - if err != nil { - return err - } - matches, err := r.depScanner.scan(f) - for _, matchIndex := range matches { - foundDeps[&r.pkgs[matchIndex]] = struct{}{} - } - f.Close() - return err - } - - // Look at the destination of symlinks to see if they point to another package. - if entry.Type() == fs.ModeSymlink { - src := path.Join(pkg.StoreName, entryPath) - dst, err := readLink(r.FS, src) - if err != nil { - return err - } - if len(dst) > 32 { - dep := r.pkgByHash[dst[:32]] - if dep == nil { - return fmt.Errorf("symlink at %s points to a missing package: %s", src, dst) - } - foundDeps[dep] = struct{}{} - } - return nil - } - - // Ignore all other file types. - return nil - }) - if err != nil { - return fmt.Errorf("error scanning %s for dependencies: %v", pkg.StoreName, err) - } - - // Recursively resolve the found dependencies to build up a DAG of packages. - pkg.DirectDependencies = make([]*Package, 0, len(foundDeps)) - for dep := range foundDeps { - if dep == pkg { - // Skip self-references. - continue - } - if err := r.resolveDeps(dep); err != nil { - return err - } - pkg.DirectDependencies = append(pkg.DirectDependencies, dep) - } - return nil -} - -// Package is a file system that contains a package's files and metadata. -type Package struct { - fs.FS - - // StoreName is the full name of the package within its store. - StoreName string - - // Hash is the package's base-32 Nix store hash. - Hash string - - // DirectDependencies are the other packages in the store that this - // package depends on. It does not contain transitive dependencies. - DirectDependencies []*Package -} - -func (p Package) String() string { - return p.StoreName -} - -// TopologicalSort resolves the dependency tree for a package and returns it as -// a slice of packages in topological order. -func TopologicalSort(pkg *Package) []*Package { - pkgs := make([]*Package, 0, len(pkg.DirectDependencies)) - seen := make(map[*Package]bool, len(pkg.DirectDependencies)) - return tsort(pkgs, seen, pkg) -} - -func tsort(sorted []*Package, seen map[*Package]bool, pkg *Package) []*Package { - if seen[pkg] { - return sorted - } - for _, dep := range pkg.DirectDependencies { - sorted = tsort(sorted, seen, dep) - } - seen[pkg] = true - return append(sorted, pkg) -} diff --git a/internal/nix/nixstore/nixstore_test.go b/internal/nix/nixstore/nixstore_test.go deleted file mode 100644 index 1300f2be9f2..00000000000 --- a/internal/nix/nixstore/nixstore_test.go +++ /dev/null @@ -1,88 +0,0 @@ -// Copyright 2024 Jetify Inc. and contributors. All rights reserved. -// Use of this source code is governed by the license in the LICENSE file. - -package nixstore - -import ( - "encoding/json" - "os" - "sort" - "strings" - "testing" -) - -// Some tests check their results against `nix path-info` output which is saved -// in the testdata directory. To update or regenerate the nix-path info output, -// run `go generate` and commit the results. - -//go:generate sh -c "nix path-info --recursive --json /nix/store/mil5crms7gfpv03vjj094zz1igvapv6i-go-1.20.2 > testdata/mil5crms7gfpv03vjj094zz1igvapv6i-go-1.20.2.json" - -func TestLocalStorePackage(t *testing.T) { - if _, err := os.Stat("/nix/store/mil5crms7gfpv03vjj094zz1igvapv6i-go-1.20.2"); err != nil { - t.Skip(`run "nix copy --from https://cache.nixos.org /nix/store/mil5crms7gfpv03vjj094zz1igvapv6i-go-1.20.2" to run this test`) - } - storePath := "/nix/store" - local, err := Local(storePath) - if err != nil { - t.Fatalf("got error for local Nix store %s: %v", storePath, err) - } - pkg, err := local.Package("mil5crms7gfpv03vjj094zz1igvapv6i-go-1.20.2") - if err != nil { - t.Fatalf("got error querying package %s: %v", pkg, err) - } - checkDependencies(t, pkg, unmarshalNixPathInfoOutput(t, pkg)) -} - -func checkDependencies(t *testing.T, got *Package, nixPathInfos map[string][]string) { - t.Helper() - - want, ok := nixPathInfos[got.StoreName] - if !ok { - t.Errorf("got unwanted package: %s", got) - } - if len(got.DirectDependencies) != len(want) { - t.Fatalf("package %s has wrong number of dependencies:\ngot: %v\nwant: %v", - got, got.DirectDependencies, want) - } - sort.Slice(got.DirectDependencies, func(i, j int) bool { - return got.DirectDependencies[i].StoreName < got.DirectDependencies[j].StoreName - }) - for i, dep := range got.DirectDependencies { - if dep.StoreName != want[i] { - t.Fatalf("package %s has unwanted dependency %s:\ngot: %v\nwant: %v", - got, dep, got.DirectDependencies, want) - } - checkDependencies(t, dep, nixPathInfos) - } -} - -func unmarshalNixPathInfoOutput(t *testing.T, got *Package) map[string][]string { - t.Helper() - - testdata := "testdata/" + got.StoreName + ".json" - b, err := os.ReadFile(testdata) - if err != nil { - t.Fatalf("got error reading %s: %v", testdata, err) - } - - var pathInfos []struct { - Path string - References []string - } - if err := json.Unmarshal(b, &pathInfos); err != nil { - t.Fatalf("got error unmarshalling %s: %v", testdata, err) - } - depsByPackage := make(map[string][]string, len(pathInfos)) - for _, pinfo := range pathInfos { - refs := make([]string, 0, len(pinfo.References)) - for _, ref := range pinfo.References { - if ref == pinfo.Path { - continue - } - refs = append(refs, strings.TrimPrefix(ref, "/nix/store/")) - } - sort.Strings(refs) - depsByPackage[strings.TrimPrefix(pinfo.Path, "/nix/store/")] = refs - } - return depsByPackage -} diff --git a/internal/nix/nixstore/scanner.go b/internal/nix/nixstore/scanner.go deleted file mode 100644 index f8ed9cdd674..00000000000 --- a/internal/nix/nixstore/scanner.go +++ /dev/null @@ -1,88 +0,0 @@ -// Copyright 2024 Jetify Inc. and contributors. All rights reserved. -// Use of this source code is governed by the license in the LICENSE file. - -package nixstore - -import ( - "io" - - "github.com/cloudflare/ahocorasick" -) - -// dependencyScanner scans through one or more readers for Nix base-32 hashes. -type dependencyScanner struct { - // matcher uses [Aho–Corasick] to look for every possible Nix - // store hash at once, which can be a large list. It's about 5x faster - // than using a regular expression and an order of magnitude faster than - // bytes.Contains. This is the same algorithm that fgrep uses. - // - // - bytes.Contains = ~21s - // - regexp.FindAll = ~2.5s - // - ahocorasick.Match = ~0.5s - // - // Optimization is warranted here because searching for hashes in large - // packages with a naive approach can take considerable time. We might - // want to add benchmarks here. - // - // [Aho–Corasick]: https://en.wikipedia.org/wiki/Aho–Corasick_algorithm> - matcher *ahocorasick.Matcher - - // buf is a reusable buffer for reading file contents. - buf []byte - - // matches contains indexes into the slice given to - // newDependencyScanner for each match. For example, if the - // storeHashes slice is ["a", "b", "c"] and matches is [2, 0], then that - // means "c" and "a" were found. It may contain duplicate indexes. - matches []int -} - -// newDependencyScanner creates a dependencyScanner that looks for a set of -// store hashes. -func newDependencyScanner(storeHashes [][]byte) dependencyScanner { - return dependencyScanner{ - matcher: ahocorasick.NewMatcher(storeHashes), - - // The buffer should be large enough to hold smaller files in a single read. - // If it's too small, then reading through all the files in a package can - // take a long time. - buf: make([]byte, 2<<24), // 16 MiB - - // Most packages won't have more than 256 non-unique references to other - // packages, and we want to avoid reallocating while scanning. - matches: make([]int, 0, 256), - } -} - -// scan reads from r looking for hashes until it encounters an error or -// [io.EOF]. It returns a slice of storeHashes indexes (as passed to -// newDependencyScanner) to indicate which hashes it found. The slice is only -// valid until the next scan and may contains duplicate indexes. -func (d *dependencyScanner) scan(r io.Reader) (indexes []int, err error) { - const hashSize = 32 // bytes - n := 0 - d.matches = d.matches[:0] - for err == nil { - // The strategy here is to prefix each read buffer with the last - // hashSize - 1 bytes of the previous read. This allows us to - // detect hashes that might be split between two reads. - n, err = r.Read(d.buf[hashSize-1:]) - if n == 0 { - // Readers should generally block instead of returning - // (n = 0, err = nil), but handle it just in case by - // reading again. - continue - } - - n += hashSize - 1 - d.matches = append(d.matches, d.matcher.Match(d.buf[:n])...) - - // Now copy over the end of this read to the beginning of the - // buffer so it prefixes the next read. - copy(d.buf, d.buf[len(d.buf)-hashSize-1:]) - } - if err == io.EOF { - return d.matches, nil - } - return nil, err -} diff --git a/internal/nix/nixstore/testdata/mil5crms7gfpv03vjj094zz1igvapv6i-go-1.20.2.json b/internal/nix/nixstore/testdata/mil5crms7gfpv03vjj094zz1igvapv6i-go-1.20.2.json deleted file mode 100644 index c5680d0652b..00000000000 --- a/internal/nix/nixstore/testdata/mil5crms7gfpv03vjj094zz1igvapv6i-go-1.20.2.json +++ /dev/null @@ -1 +0,0 @@ -[{"deriver":"/nix/store/yypkli239c5yy80j7f2diimwih3rla66-bootstrap_cmds-121.drv","narHash":"sha256-srfht7WtlYFIoGrcyDQpRyZ7Qa9/8/MUJx0bj8Shpvo=","narSize":245008,"path":"/nix/store/07iz4261mvq1zz9npmn5yqyyj6n1xz07-bootstrap_cmds-121","references":["/nix/store/21fn46279sjpqqqqsy2cnkxr3rqzbwq6-apple-framework-CoreFoundation-11.0.0","/nix/store/ka6rabx4lz7m3habrjhh8hvbgxbz8r98-bash-5.2-p15"],"registrationTime":1680294962,"signatures":["cache.nixos.org-1:dZlnTMOOyYO5HFxxqV82zkQoIkuVXganfDSFXdkvsQ2PLXuWn8qQn/yM3E+KqZ9q1VeQR5mUAordSNDKlW7NCw=="]},{"deriver":"/nix/store/xvbr5nckh5jpalg75gjw7mizbqjhajl3-mkdep.sh.drv","narHash":"sha256-gZSe8F/4y6IUNU7bWSn0XInoLorZF+3XL1o6qh2+nFg=","narSize":3016,"path":"/nix/store/0bqhsb3qawa3n5mx1dhgwfqz484y28kr-mkdep.sh","references":[],"registrationTime":1667509121,"signatures":["cache.nixos.org-1:tfe91n5PMVY2ZuhVYEYcRcbZkWoT4qhlwo78BoD09CpTBSi0qkGhh5gyBA2lRNnKnIX+ctBghiWFiIwPyJbtDw=="]},{"deriver":"/nix/store/myfpiybiaiqkld5mfrjbw3wvn9r4v6dm-expand-response-params.drv","narHash":"sha256-JrKvnqY2xBKEyeEU92tyRjjnUkETFGGmUaEHHyxRtjc=","narSize":52480,"path":"/nix/store/0vmh9wsqldbhc4m0fzlmri6jymn1y1y5-expand-response-params","references":[],"registrationTime":1680294962,"signatures":["cache.nixos.org-1:xGDA8oOk1v5jJ74uI9OurJoann2hL82uBPsXGYlTNEren/IJqHFuo3yHQqfoI8aLITUGDATNDeY1ZlQiEDN9Dg=="]},{"deriver":"/nix/store/hxc33kbpnav6y5vxvyc53yv7qmkasj0i-libSystem-11.0.0.drv","narHash":"sha256-R3ziA5ByjBO4Y0YwZ9bzdIszNZoKP7nTD4QO+MRLPxc=","narSize":11313512,"path":"/nix/store/0xjfx5w9xhnsx2hharl3mpdmrf07q9fm-libSystem-11.0.0","references":["/nix/store/0xjfx5w9xhnsx2hharl3mpdmrf07q9fm-libSystem-11.0.0"],"registrationTime":1680294962,"signatures":["cache.nixos.org-1:HMVT8Yc5dueX1o51Gyw9meUfE87blBh+PTerYE/3zbOEPo8oVsE+Y8KtfNT8p1qwb17DsuNKUjlAC7QqZ00jDw=="]},{"deriver":"/nix/store/1xp025l3g3waa0qai7qcfgm4cbc6np9c-libcxx-11.1.0.drv","narHash":"sha256-nwJll5bdS8N28yMrfPWbYVVSfRii7Lc+wbErKnNsD7Y=","narSize":4772544,"path":"/nix/store/1wamlhkl3q15b7pdim85vqkmz053fwlg-libcxx-11.1.0-dev","references":["/nix/store/y56hqp2dgrh9hi9xyd88n0pcl6gn895d-libcxx-11.1.0"],"registrationTime":1680294962,"signatures":["cache.nixos.org-1:ZRctnUtqJbJkIwmivPpCDa87+HVDb+8cpHM67Gb3+hQivoXlbq35hPpDCGjVzH1ku6aLZXcdOB5LyXQkGMZUDA=="]},{"deriver":"/nix/store/lf4095fpqhr9qfaf9yhj8qj6sp88cqb9-apple-framework-CoreFoundation-11.0.0.drv","narHash":"sha256-5wwSc9mtU9PQr/Abx7oOsMfKcqGL/hBduYlQPi/N68Y=","narSize":695784,"path":"/nix/store/21fn46279sjpqqqqsy2cnkxr3rqzbwq6-apple-framework-CoreFoundation-11.0.0","references":["/nix/store/3pd5fbv4did9ya9mjbcj25rfmdk021sm-libobjc-11.0.0"],"registrationTime":1680294962,"signatures":["cache.nixos.org-1:Dc1naJYMUAPgIs+tNWcL0IKwMgGvHwCd6BeHw6ZmUdnzyIc05kaDHkTigZuAgxT+pNe4KWRsKV7f0X4BZatvAw=="]},{"deriver":"/nix/store/zhfbch65lwylpmjhhqn0s1qcw4mi2aga-libcxxabi-11.1.0.drv","narHash":"sha256-/kWbmMFZRrXvcDdbL6Y7Xg+bUB1tXVpuzt51JyHNvko=","narSize":10848,"path":"/nix/store/21knak232h33cha4a16k6gzgai23c0ss-libcxxabi-11.1.0-dev","references":["/nix/store/gs6iqzg3cswfsxwnwcvsnf6fn76wpbr2-libcxxabi-11.1.0"],"registrationTime":1680294962,"signatures":["cache.nixos.org-1:yjk/Sv+aBpQ3rCLpIo0jf2MxKimXK1FXwoo6wVpBMkmJZ0Wg9PZ5EurWzq6RaDbNvvcp9WdR0cY+iZ7hmZN9Dw=="]},{"deriver":"/nix/store/zizzw3hcbsivj3cjhkcf3xfxnn8xmmz7-apple-framework-OpenDirectory-11.0.0.drv","narHash":"sha256-1F5PbPq7acY75bIySBerTkOwhyYRKSoTGfgKs2UI53Q=","narSize":366696,"path":"/nix/store/2hl6k0mx0bl167kf4ggjaz0pcxbsz1bi-apple-framework-OpenDirectory-11.0.0","references":["/nix/store/2hl6k0mx0bl167kf4ggjaz0pcxbsz1bi-apple-framework-OpenDirectory-11.0.0"],"registrationTime":1680294962,"signatures":["cache.nixos.org-1:aY7rqK+qXIietGYuBqRyGTYnO03VV9XS9g1QhL5+ke6ReJLuzHvi9bgmvHAC1iilkycbm/bOo+5yQi5gMVsdBg=="]},{"deriver":"/nix/store/c4rd5dsgikjb7qxj6w9kp4gygzsbgbzq-apple-framework-CoreAudioTypes-11.0.0.drv","narHash":"sha256-OjTMR8SrNHfeqhh7JpKDqo3w2o7tm7NvMNW9OXo3sA0=","narSize":89160,"path":"/nix/store/33w6vyz4gd3idqg5qy2lmpkn2l8hbb7i-apple-framework-CoreAudioTypes-11.0.0","references":[],"registrationTime":1680294962,"signatures":["cache.nixos.org-1:ARcMz0u6xgViJ3NgOQKXliffNCZvjTKGfelsVo2V07ZJil5wsdE3jR4nXACuaRdx/FT5Kg+RdF5oTJj/1Hg/Aw=="]},{"deriver":"/nix/store/d7pnlcy1yg6ssjvbgh409rrzf8bhjszw-cctools-port-973.0.1.drv","narHash":"sha256-X6g7A7rCFyw5U35zSfUxmTCL2VbOVpj3GRyYQ0oJYjw=","narSize":11147288,"path":"/nix/store/35vkq2dc6v7zpj83zxavc5x78wpz5ak3-cctools-port-973.0.1","references":["/nix/store/35vkq2dc6v7zpj83zxavc5x78wpz5ak3-cctools-port-973.0.1","/nix/store/gs6iqzg3cswfsxwnwcvsnf6fn76wpbr2-libcxxabi-11.1.0","/nix/store/jx8f6kj7pv57x54zihb3wp3xglbwq5za-libtapi-1100.0.11","/nix/store/y56hqp2dgrh9hi9xyd88n0pcl6gn895d-libcxx-11.1.0"],"registrationTime":1680294964,"signatures":["cache.nixos.org-1:V/BfjNSQKuHjMYg9xMfD2ScxlZyTWRqwAT5BBc96zutbCEP1WLDSdRZX+KI3Rzy7lT+XhBLJmK632MuMBEXxBQ=="]},{"deriver":"/nix/store/yrpx8k6r05p18ar670ncrd6w2xaw2q65-llvm-11.1.0.drv","narHash":"sha256-mEiRZROZb2lENugLVI7vnQ+TllW8lptThUD3ZVs5Jms=","narSize":203877520,"path":"/nix/store/3bn6wnswkqmaiv0px062bblxbcqm5xhv-llvm-11.1.0-lib","references":["/nix/store/3bn6wnswkqmaiv0px062bblxbcqm5xhv-llvm-11.1.0-lib","/nix/store/3i2pp8m7cy8s2w23f9lzj4whl4grkpfg-zlib-1.2.13","/nix/store/7ha3vvxvbcf6nx1qkx16gs8qzaiwzi07-libxml2-2.10.1","/nix/store/ay0icgbhay57540wh6yy12sh5ngwgzy9-ncurses-6.4","/nix/store/gs6iqzg3cswfsxwnwcvsnf6fn76wpbr2-libcxxabi-11.1.0","/nix/store/lpv1rr58zr5xa2q60s1x4792pgb6w7fr-libiconv-50","/nix/store/xc88ky1w798ncij3jr12gqcv2w9njyy2-libffi-3.4.4","/nix/store/y56hqp2dgrh9hi9xyd88n0pcl6gn895d-libcxx-11.1.0"],"registrationTime":1680294966,"signatures":["cache.nixos.org-1:QVXdonPkgB4glNQs7vWW2ZdgVSJBrA9n6tRcL7EY93DnPK26r/VduHwiK64sYCPTmpAh8VOz+z3V38VB9FkaAA=="]},{"deriver":"/nix/store/i214f537xq4788bybi2hilrsvbz1czj6-zlib-1.2.13.drv","narHash":"sha256-vmggPjMcpqUmzg0ubvNL9RD+Gvy1hKCr8xnEzTgCxwU=","narSize":424760,"path":"/nix/store/3i2pp8m7cy8s2w23f9lzj4whl4grkpfg-zlib-1.2.13","references":["/nix/store/3i2pp8m7cy8s2w23f9lzj4whl4grkpfg-zlib-1.2.13"],"registrationTime":1680294963,"signatures":["cache.nixos.org-1:f8AZO+9Oj0ZMV8gYuyeSrUzMMnQvdqF02AW691vi5pAOwXpf0t+oMRNpPS5vy7y+Y8i28IWai610na0/RUc9AQ=="]},{"deriver":"/nix/store/bn5mpc24pypfhh3j326ivga8h5raiqdp-libobjc-11.0.0.drv","narHash":"sha256-GzBUJqGfqgh199IimEmtu1L1xdkTxKLP1PkdF+G8dT8=","narSize":268696,"path":"/nix/store/3pd5fbv4did9ya9mjbcj25rfmdk021sm-libobjc-11.0.0","references":[],"registrationTime":1680294962,"signatures":["cache.nixos.org-1:hXsUzzwX6isog0diURCxl+aRPcoAKfyZfkBFXL8rxWGI+ju0J6cngHMN++9tRYYi+3yBvTvXeTsNb66esDYRDw=="]},{"deriver":"/nix/store/f0ian9wpwkib7mc53qpcq5vlxswqqjxr-gettext-0.21.drv","narHash":"sha256-7s2E6SxluWzP8r/Gz2UizIzeAPQLFcsN+kbYYuHCYeA=","narSize":9361768,"path":"/nix/store/41c100x9gdslcrcpz5gq0pjgy8a7klhs-gettext-0.21","references":["/nix/store/41c100x9gdslcrcpz5gq0pjgy8a7klhs-gettext-0.21","/nix/store/gs6iqzg3cswfsxwnwcvsnf6fn76wpbr2-libcxxabi-11.1.0","/nix/store/ka6rabx4lz7m3habrjhh8hvbgxbz8r98-bash-5.2-p15","/nix/store/lpv1rr58zr5xa2q60s1x4792pgb6w7fr-libiconv-50","/nix/store/y56hqp2dgrh9hi9xyd88n0pcl6gn895d-libcxx-11.1.0"],"registrationTime":1680294962,"signatures":["cache.nixos.org-1:VpiS0yfzJ6rd8/HF7yoCYW5cFTpd0NxVPYaRby233IiJ9OoLxgNqfR8WS3dYn/yHMSPmUEUfNjd6Yc0c1pKZAA=="]},{"deriver":"/nix/store/gk3pb086kdnk383c4ss69hgha7krmsks-apple-framework-CoreData-11.0.0.drv","narHash":"sha256-WKrUikUyMo0ohp9F8RrNrnN1Ao6Yv9+HpLtxddqbF2Y=","narSize":491688,"path":"/nix/store/45dl40acmas1wkh2kva1db5bry1rwszs-apple-framework-CoreData-11.0.0","references":["/nix/store/90pcbb199w35fw42lbk6y4cc61lmmy15-apple-framework-CloudKit-11.0.0"],"registrationTime":1680294962,"signatures":["cache.nixos.org-1:KeMsj1TznuojNCuCRPG2bRdVw+diyIvG8vPy14KHcHJ+ZAZ7vMCWHsT1OfOswh+oxI3eshhkrsTOabzDc3kxDA=="]},{"deriver":"/nix/store/s8l5ffsrzbb74sjlr99y15w4q1g1ir0z-apple-framework-CoreGraphics-11.0.0.drv","narHash":"sha256-je0JzC1GZAGQFUeSQoa0z8LeGSfGpYEetlxMs5pOJZw=","narSize":921856,"path":"/nix/store/4al6fm8vlmv3v7749sfqvj34vnhsfbsq-apple-framework-CoreGraphics-11.0.0","references":["/nix/store/5ij3n8sd1wkjsfayidkg2yxpm2b40jw5-apple-framework-SystemConfiguration-11.0.0","/nix/store/5ncnv0kgz3fzf3bnq5bl35lw18yvw706-apple-framework-IOKit-11.0.0","/nix/store/sllbiczqxnlg732nj0sn21b1rgsac5mg-apple-framework-IOSurface-11.0.0","/nix/store/sx4lwrgyay49ynkpp2pq71s0l40mj1f9-apple-framework-Accelerate-11.0.0"],"registrationTime":1680294963,"signatures":["cache.nixos.org-1:3qD9Cz5rBlmtkpl7/Fy7ZGJmHcNsg2uD6+wq/f4Y4EV+JwHszDcqYVW/bHolNeYerGyPBbvBxrrMKv11mZlQAg=="]},{"deriver":"/nix/store/qqml9lvkq747anbl62yabbhj6cfdm6cs-flex-2.6.4.drv","narHash":"sha256-brDisbIr3dQZs+JrXYHNaoRGtquQbmvOkv6FreNYgTg=","narSize":1261968,"path":"/nix/store/4kg1hjr6x01hrcq266qqh32zhafws1i7-flex-2.6.4","references":["/nix/store/21fn46279sjpqqqqsy2cnkxr3rqzbwq6-apple-framework-CoreFoundation-11.0.0","/nix/store/4kg1hjr6x01hrcq266qqh32zhafws1i7-flex-2.6.4","/nix/store/5lqiv4ip7gqdv6hdz10pgr8w7mx26zlp-gnum4-1.4.19","/nix/store/hr6cwwvfmfp3c131g1xqvjxp0d3cf4n5-gettext-0.21"],"registrationTime":1680294963,"signatures":["cache.nixos.org-1:lPXXhaYuXtSnJj25GOP10e8E0SRDev0iZ9NBYiZ9XTfYyuCC5Y4HGGcLk8Oa9IeJBVizJjzS04NM5TA5tGyVBg=="]},{"deriver":"/nix/store/g34aw1lk434jhiw35fa1yqkqrplg1inv-apple-framework-SystemConfiguration-11.0.0.drv","narHash":"sha256-ly+DVAoXp59jwOx769KwgOqHa43OfIqxRIWAbxzQlLc=","narSize":323432,"path":"/nix/store/5ij3n8sd1wkjsfayidkg2yxpm2b40jw5-apple-framework-SystemConfiguration-11.0.0","references":["/nix/store/q57wwyd23i6msdawg6casxc3cgvxdsaa-apple-framework-Security-11.0.0"],"registrationTime":1680294962,"signatures":["cache.nixos.org-1:oyVbUbkWuns05E4YvNcxUDlOQWfmOh7UcaU6GVxHl/g6fVjQ8DhcCb+jeisaXXcySVCoBx5bh6xIvX06/frSAQ=="]},{"deriver":"/nix/store/g7825hlvfrhr861jnifdrjzc5fd8lc79-gnum4-1.4.19.drv","narHash":"sha256-8weKLpb409Lv2gwtMVra8TP0m4B5DRceLwvGRq16dZk=","narSize":691592,"path":"/nix/store/5lqiv4ip7gqdv6hdz10pgr8w7mx26zlp-gnum4-1.4.19","references":["/nix/store/21fn46279sjpqqqqsy2cnkxr3rqzbwq6-apple-framework-CoreFoundation-11.0.0","/nix/store/ka6rabx4lz7m3habrjhh8hvbgxbz8r98-bash-5.2-p15"],"registrationTime":1680294963,"signatures":["cache.nixos.org-1:2jfyky3ms4E4cIz7Ih65paMwXLS72mfvSy3S5pTRlaOdMLcFLCx+yhYewkMozMWZz5jbfx0TM8JxdhFrrq63Bg=="]},{"deriver":"/nix/store/87d5jwl1bcfppqy1ghkg7l30f7vgmki6-apple-framework-IOKit-11.0.0.drv","narHash":"sha256-QO2BPsTxH22SWRqX8sgPF1zlAdEstPNrARE0GFzR6vQ=","narSize":2724168,"path":"/nix/store/5ncnv0kgz3fzf3bnq5bl35lw18yvw706-apple-framework-IOKit-11.0.0","references":[],"registrationTime":1680294962,"signatures":["cache.nixos.org-1:/aPG1KB09lVkRYo/mgvU0oM/+Pg5N8KtHFzY1O/6HsHlchZidR8ln85t5AuZfCeH+fWuPNOML3yyExudV5n4BA=="]},{"deriver":"/nix/store/506mqif007blxp2ncv0cjha2ihqq75bl-apple-framework-CoreAudio-11.0.0.drv","narHash":"sha256-UPjKo54/mcUfkS7NsaraboKPSXp9UfWBlV6viBfj5nc=","narSize":533456,"path":"/nix/store/656hkg3497rx0wcssd895k4pvwc4jzag-apple-framework-CoreAudio-11.0.0","references":["/nix/store/33w6vyz4gd3idqg5qy2lmpkn2l8hbb7i-apple-framework-CoreAudioTypes-11.0.0","/nix/store/5ncnv0kgz3fzf3bnq5bl35lw18yvw706-apple-framework-IOKit-11.0.0"],"registrationTime":1680294962,"signatures":["cache.nixos.org-1:/3qG8pEYl8TfvrPTsRsJxzGoVzrUzFMpTOeau+DhhEXelA+zhHtFp7GTtkrcTKUpfb30ASwy7HkyH2vsFXPXDA=="]},{"deriver":"/nix/store/b5qsfjhxl5j07rr7dvsv52vrqwxlcdxs-SDKs.drv","narHash":"sha256-6LQSlFRIYMH5GQHmcMXIPhQ0Ar42xN+igZTTxUcbgPI=","narSize":3016,"path":"/nix/store/6jdcd2hw2jc99iy72zb80si1q41l008h-SDKs","references":["/nix/store/6jdcd2hw2jc99iy72zb80si1q41l008h-SDKs"],"registrationTime":1680294961,"signatures":["cache.nixos.org-1:xxPAI8Py7H9su6Hx3Ivk+Gil3agE97OI8qgZD1XyekBwDG9H7VaN+Pvnj2EVO3Dd/5YexwnTcZvM1SntrLUiCw=="]},{"deriver":"/nix/store/5jfhhbg4s32wpd8xglh0y8qikrkqdx3b-libxml2-2.10.1.drv","narHash":"sha256-Is0WRHqMHMT2MItGbRUYk68CkJaF0I6oc3nfQ5vNt44=","narSize":1331704,"path":"/nix/store/7ha3vvxvbcf6nx1qkx16gs8qzaiwzi07-libxml2-2.10.1","references":["/nix/store/3i2pp8m7cy8s2w23f9lzj4whl4grkpfg-zlib-1.2.13","/nix/store/7ha3vvxvbcf6nx1qkx16gs8qzaiwzi07-libxml2-2.10.1","/nix/store/lpv1rr58zr5xa2q60s1x4792pgb6w7fr-libiconv-50"],"registrationTime":1680294963,"signatures":["cache.nixos.org-1:gULptIbKvm8iF7fEFGv19nbMHKyPINv51k5kgz4jBUJpKt1JKd87WoCe0RvBROQn+I7+A7xBtSu9TECtghsHAw=="]},{"deriver":"/nix/store/s2vvl133y1rls21n2146758dcnf9264v-iana-etc-20221107.drv","narHash":"sha256-+2VggbOKLdFD+09B59EeLNYwTgPPEnDwCvBPk4VBqCI=","narSize":569440,"path":"/nix/store/7l1jnzfkx3g4idjyi7vn25rmszl8lizz-iana-etc-20221107","references":["/nix/store/7l1jnzfkx3g4idjyi7vn25rmszl8lizz-iana-etc-20221107"],"registrationTime":1680294962,"signatures":["cache.nixos.org-1:Md4kuIMCbUYQGUfcYFQOMMyTez/U3yjt5LIdI9M18NZIlkyRl7CNV4fX+5svv9jj32rxspqrlsuhBT0Jol5eAA=="]},{"deriver":"/nix/store/kc66m2sjj9ggv6cb3xzsa6qz054i9955-apple-lib-libDER.drv","narHash":"sha256-7ksGREOlv00aSTuAweahrsKXjoy05j2ZrEKmV3GtWBs=","narSize":5584,"path":"/nix/store/7p8qgl96a2grvwaaxnpw678m4f99l86v-apple-lib-libDER","references":[],"registrationTime":1680294962,"signatures":["cache.nixos.org-1:KTpt91CXKR8KYPYtwoEvj4+jGETZ2+uICu5D7Ba6Nexor03VtztwpGB2pCDplHSBxkBrPa75qurdu2hodOBPAw=="]},{"deriver":"/nix/store/3fjs7jkb6f7bfk9pm2p652zcms82062i-compiler-rt-libc-11.1.0.drv","narHash":"sha256-V0n+24KHLJCJt8PwWyXaCKAFwqoeK4TBPxeooxBQVUE=","narSize":334416,"path":"/nix/store/81jnzlpii91vz9y8rwb7g095zk2jj2qr-compiler-rt-libc-11.1.0","references":[],"registrationTime":1680294962,"signatures":["cache.nixos.org-1:VUbXEdA7/qKgC4n75qEXC4ZW43ZwQzxv71I2crvBlHmWoEYF1D7v9cL15yWNfI6GlshfYX/n6hCCJylVmnyhAg=="]},{"deriver":"/nix/store/k95ysskyksbj7z0k3r7klm0vksmc392f-apple-framework-Combine-11.0.0.drv","narHash":"sha256-lM0htCwDSRHb3FoChBxRpibfy2x8Aw6rPK/EOy0phXQ=","narSize":4013832,"path":"/nix/store/8ar7mqa5hqdw2xrw6a0i7p272rkvlgg1-apple-framework-Combine-11.0.0","references":[],"registrationTime":1680294962,"signatures":["cache.nixos.org-1:cu76QzBhF7ij3sfVMR9mqxUxD7ncANFfjFu3mYa5hiGHbRX7wuRuvYttvHu5ClPl0S5GaVZNUIhmqv5j+zdCCw=="]},{"deriver":"/nix/store/mhr2x0xmgf4vyb67lqmhlfi47wzw68zm-apple-framework-CloudKit-11.0.0.drv","narHash":"sha256-nlfs7PlNoRfdVgOfATrWx+p1qhniA7vGybN6vWGFGs4=","narSize":438504,"path":"/nix/store/90pcbb199w35fw42lbk6y4cc61lmmy15-apple-framework-CloudKit-11.0.0","references":["/nix/store/hzjxfgdxac2bvmkw40bsyxm3kh73kl70-apple-framework-CoreLocation-11.0.0"],"registrationTime":1680294962,"signatures":["cache.nixos.org-1:6ibZZ8+5S1he5gEjjZiEWGRJmPzmwy4El4nyI8E3Rixp1fqouOVpiX2YRyFxSS3s6BHV3uKgZmciMXfRt80pCg=="]},{"deriver":"/nix/store/93h4vrz9a8gfi9xac1jvd06zwq3rdlk5-ctags-816.drv","narHash":"sha256-R0etXvW8/rEPQ4QY63x7F7ENhU141fnCTKJqyTtoIxI=","narSize":314464,"path":"/nix/store/9lsmm2i87l5ia9hhcxr26x1hz09bv2ql-ctags-816","references":["/nix/store/21fn46279sjpqqqqsy2cnkxr3rqzbwq6-apple-framework-CoreFoundation-11.0.0"],"registrationTime":1680294962,"signatures":["cache.nixos.org-1:DemQJq06Oq0+3ok5hhnVvvLT2zXZyqkWMynGH3lmCII+x08yj5SOwGNA9/XisUVcH+3WpfhKVhllaxyiMEWcBw=="]},{"deriver":"/nix/store/6zpdam808p9r4yk88vvw7wm5hnzklp3k-xcbuild-0.1.2-pre.drv","narHash":"sha256-by630VrZia9FaMDJXm0wMS1jOn6/bjVVzLBWS9dqq7A=","narSize":5103640,"path":"/nix/store/a8kldaazsbvv07gjpx0nr9bqx3q9fk47-xcbuild-0.1.2-pre","references":["/nix/store/21fn46279sjpqqqqsy2cnkxr3rqzbwq6-apple-framework-CoreFoundation-11.0.0","/nix/store/3i2pp8m7cy8s2w23f9lzj4whl4grkpfg-zlib-1.2.13","/nix/store/a8kldaazsbvv07gjpx0nr9bqx3q9fk47-xcbuild-0.1.2-pre","/nix/store/gs6iqzg3cswfsxwnwcvsnf6fn76wpbr2-libcxxabi-11.1.0","/nix/store/la358rlj7ck6z9i4cwwvla05zmdrrp42-libxml2-2.10.3","/nix/store/lpv1rr58zr5xa2q60s1x4792pgb6w7fr-libiconv-50","/nix/store/y56hqp2dgrh9hi9xyd88n0pcl6gn895d-libcxx-11.1.0"],"registrationTime":1680294964,"signatures":["cache.nixos.org-1:/NlqSlEm7NLl6isLKLGJQhfhjvvv7DTPt3Em6yjDn4OvR7oEMKV7gW6w6xiGWhTyOGjAcVF5oh/JRPmY0rDcDQ=="]},{"deriver":"/nix/store/8v9scp0ycmg1dhaw7qyql979q0rc6i6v-ncurses-6.4.drv","narHash":"sha256-CPrLZ2TRekKti4q62osVgM+ld16QfltsoCTKPF114eQ=","narSize":3901776,"path":"/nix/store/ay0icgbhay57540wh6yy12sh5ngwgzy9-ncurses-6.4","references":["/nix/store/ay0icgbhay57540wh6yy12sh5ngwgzy9-ncurses-6.4"],"registrationTime":1680294962,"signatures":["cache.nixos.org-1:4/2ELn7bCbu4QYxigVCHj8QsUJwMW39qfErmlYy+nZ5rZuaUR8dr/4NLNV7By6fOLgrQuT/YE6ND903DQ/+3CQ=="]},{"deriver":"/nix/store/c60cqigilvyjdrmgcl3pcm0jx4bnbly5-openssl-3.0.8.drv","narHash":"sha256-VGQFENhG9P+ZAl2XyCRn1SQ2OeMTDlSeUwo/zSNYUSM=","narSize":4506392,"path":"/nix/store/ay24bgl7phsf067kfnvg5czbqq7gl6n0-openssl-3.0.8","references":["/nix/store/ay24bgl7phsf067kfnvg5czbqq7gl6n0-openssl-3.0.8"],"registrationTime":1680294962,"signatures":["cache.nixos.org-1:FdfKyPj+D5UBvIaFIs16LxBk27qWL/2k9OZPPJnuKuHBQhE6Tgtl6tABP+6X+sV1BOQ0AsqdVsSPxjihqvBWAA=="]},{"deriver":"/nix/store/sjvazfpg9npzza9wha77al3qlhhfhrv0-bison-3.8.2.drv","narHash":"sha256-eTNKa4ddvcJx5WUH8PJYunVdOeemprdha0vjLtEPyFw=","narSize":2484432,"path":"/nix/store/bq8ybmdm15rhmm7ps3qppikra3gvsqkh-bison-3.8.2","references":["/nix/store/21fn46279sjpqqqqsy2cnkxr3rqzbwq6-apple-framework-CoreFoundation-11.0.0","/nix/store/5lqiv4ip7gqdv6hdz10pgr8w7mx26zlp-gnum4-1.4.19","/nix/store/bq8ybmdm15rhmm7ps3qppikra3gvsqkh-bison-3.8.2","/nix/store/ka6rabx4lz7m3habrjhh8hvbgxbz8r98-bash-5.2-p15"],"registrationTime":1680294963,"signatures":["cache.nixos.org-1:x8fCYbUz+uCaWtUYRK8tDK6onIEBjb80zF4sOKvCme4Lpjh3fPkRT4byBMa/kUjftJzKCs7VfPfctJy7HQySCg=="]},{"deriver":"/nix/store/093v6xfya0dva221nh1zkhjpn6c43xf9-apple-framework-DiskArbitration-11.0.0.drv","narHash":"sha256-Em9DfqLJYwZ/Kh2aMMBEg0gI7hUQqiIIvswAm2YYaj4=","narSize":50696,"path":"/nix/store/byzgp03rhiybvnr9n42i8b4931kaqchb-apple-framework-DiskArbitration-11.0.0","references":["/nix/store/5ncnv0kgz3fzf3bnq5bl35lw18yvw706-apple-framework-IOKit-11.0.0"],"registrationTime":1680294962,"signatures":["cache.nixos.org-1:Wek1GIsIPMhWJ21qFU1FkgoN9N8C8WdVse7URcM1bmse7vrz51YHSG3sXCDSxIkDdpy1TKbPqwA9SMECcy/MCw=="]},{"deriver":"/nix/store/jsn5qv8xf6l88fv73wizf095a6ygrr80-indent-2.2.12.drv","narHash":"sha256-fK1Lu+b0ZDppk2fSbB8FAVd+OlboPuCr1jo+Vgg8reY=","narSize":612184,"path":"/nix/store/cfnv1hx6n0n20kgqbdgvv2d4ziydmhwm-indent-2.2.12","references":["/nix/store/21fn46279sjpqqqqsy2cnkxr3rqzbwq6-apple-framework-CoreFoundation-11.0.0","/nix/store/cfnv1hx6n0n20kgqbdgvv2d4ziydmhwm-indent-2.2.12"],"registrationTime":1680294963,"signatures":["cache.nixos.org-1:j6QqpBFVGbGYlQHqi3nTnruUTEcnQ+E7laVKfjU35r2mSsjTS7qKfYm6PfelDpmKbnFPvgSxpt3stmCOEfPoCg=="]},{"deriver":"/nix/store/025mip155cswc9wv5rnpcvs7948kmqmw-clang-wrapper-11.1.0.drv","narHash":"sha256-H4dCR+Sn1BQKYWFcE58rC2zYxrehBIG9dDdGAE8Boa0=","narSize":57464,"path":"/nix/store/cy78am69kj3d2r286rd7wg0cv48gqa3z-clang-wrapper-11.1.0","references":["/nix/store/0vmh9wsqldbhc4m0fzlmri6jymn1y1y5-expand-response-params","/nix/store/0xjfx5w9xhnsx2hharl3mpdmrf07q9fm-libSystem-11.0.0","/nix/store/1wamlhkl3q15b7pdim85vqkmz053fwlg-libcxx-11.1.0-dev","/nix/store/21knak232h33cha4a16k6gzgai23c0ss-libcxxabi-11.1.0-dev","/nix/store/81jnzlpii91vz9y8rwb7g095zk2jj2qr-compiler-rt-libc-11.1.0","/nix/store/cy78am69kj3d2r286rd7wg0cv48gqa3z-clang-wrapper-11.1.0","/nix/store/h2hd63vq9js32ggqx9nvhsjqx4b2ckss-compiler-rt-libc-11.1.0-dev","/nix/store/ka6rabx4lz7m3habrjhh8hvbgxbz8r98-bash-5.2-p15","/nix/store/nyrn55869kmx1bn1rx4mhk18irypbh1r-clang-11.1.0","/nix/store/qr4zarpiaf3fk2bm01gnb33grg2nwwrh-coreutils-9.1","/nix/store/v1vhsx3ss1ap9nn520y9dmibhqjpnf91-clang-11.1.0-lib","/nix/store/v3ks390idh1xqnh3r0zlkqc86pcy7lva-cctools-binutils-darwin-wrapper-973.0.1","/nix/store/y56hqp2dgrh9hi9xyd88n0pcl6gn895d-libcxx-11.1.0","/nix/store/zmfr0yl6sab6q9rkd0shrpviq0f4i7mi-gnugrep-3.7"],"registrationTime":1680294968,"signatures":["cache.nixos.org-1:SHmtEahnjErrWWO4xT1vTNSeZ6FYa1eJrxkI9i7Wms9orG39TfX5jtSaiphYpfYzHeSd4xO7oYuSy/yNVU67Ag=="]},{"deriver":"/nix/store/1yb4qfylknssgyqv3h01vr9mvf9bzr08-Toolchains.drv","narHash":"sha256-9V/3ua0nxUwgEIIcN0DCBG+0KnvP1BI5eq53C20sDCQ=","narSize":16432,"path":"/nix/store/d6fa6g9i8x410kankvzvmzrq90pb82qh-Toolchains","references":["/nix/store/07iz4261mvq1zz9npmn5yqyyj6n1xz07-bootstrap_cmds-121","/nix/store/0bqhsb3qawa3n5mx1dhgwfqz484y28kr-mkdep.sh","/nix/store/35vkq2dc6v7zpj83zxavc5x78wpz5ak3-cctools-port-973.0.1","/nix/store/4kg1hjr6x01hrcq266qqh32zhafws1i7-flex-2.6.4","/nix/store/5lqiv4ip7gqdv6hdz10pgr8w7mx26zlp-gnum4-1.4.19","/nix/store/9lsmm2i87l5ia9hhcxr26x1hz09bv2ql-ctags-816","/nix/store/bq8ybmdm15rhmm7ps3qppikra3gvsqkh-bison-3.8.2","/nix/store/cfnv1hx6n0n20kgqbdgvv2d4ziydmhwm-indent-2.2.12","/nix/store/cy78am69kj3d2r286rd7wg0cv48gqa3z-clang-wrapper-11.1.0","/nix/store/d6fa6g9i8x410kankvzvmzrq90pb82qh-Toolchains","/nix/store/h4pzmkzjqwj7479wzcks7vjgq07grm0p-unifdef-2.12","/nix/store/hh91b8qb8ghv5jjpiyficqgd4rczfy5a-gperf-3.1","/nix/store/nq3fp1bjaizwxv6j7gqnza6s8sdiamm8-cctools-binutils-darwin-973.0.1"],"registrationTime":1680294968,"signatures":["cache.nixos.org-1:xKq/30ZKmYmwmotzCTMYAhbqfZbiBb8dnSmu38K58tnGrI1NL5O+HZwJ9RdXqzxn91ro7BVTVrgMJd7uoXQCDw=="]},{"deriver":"/nix/store/103jm910v7f8dg5n7l1vmzdbfqsc9ji5-signing-utils.drv","narHash":"sha256-f7Ti8HxbWButGeRXLIPOapONKaYgSxL9weLimbIcnUo=","narSize":1536,"path":"/nix/store/d6lbh51mr15c99h0vjcvh98ibnd8k21z-signing-utils","references":["/nix/store/35vkq2dc6v7zpj83zxavc5x78wpz5ak3-cctools-port-973.0.1","/nix/store/kg0wv92mmn7bbyrc0hfg4k8mr5z4pn6r-sigtool-0.1.3"],"registrationTime":1680294964,"signatures":["cache.nixos.org-1:B8mjbawOrdllH15EaSffcHadta/s5iMr5KMjliMTay262zdDCWCVt7RYoCToht/uHFYUuHNCZv3PwYPC8nE0Aw=="]},{"deriver":"/nix/store/yxq7p946brbndl0vi230y5w654pk6n9c-apple-framework-CoreWLAN-11.0.0.drv","narHash":"sha256-5/X6cdinjYPLpBGDl1wkoLnrStpvYf+jMM3VPKrSMYY=","narSize":98104,"path":"/nix/store/d6q9q9gqrpbl799j70jk0rp3sff2andd-apple-framework-CoreWLAN-11.0.0","references":["/nix/store/i62176fkd5yg2qbxrqb4gd1bbijzmh2m-apple-framework-SecurityFoundation-11.0.0"],"registrationTime":1680294962,"signatures":["cache.nixos.org-1:7H1N4kR1Rq7Hq3JFx2R6PGHO4SYxX6g2rBYYq4YOdvW8GGB27yBInLomTCo6p+jyAmloTifnJA6h8aMwDtJsDg=="]},{"deriver":"/nix/store/zns78q3s255g5qik3iji5gzchhixhr5f-apple-framework-Foundation-11.0.0.drv","narHash":"sha256-B4Do4pb2YcY07IPWiM8KE8o4bXcFXypIhR/wnEo20KA=","narSize":7248608,"path":"/nix/store/f7p955pdwhgirijk33mash7rmlkzklkx-apple-framework-Foundation-11.0.0","references":["/nix/store/3pd5fbv4did9ya9mjbcj25rfmdk021sm-libobjc-11.0.0","/nix/store/5ij3n8sd1wkjsfayidkg2yxpm2b40jw5-apple-framework-SystemConfiguration-11.0.0","/nix/store/8ar7mqa5hqdw2xrw6a0i7p272rkvlgg1-apple-framework-Combine-11.0.0","/nix/store/k7rk29pxrsy3ry29k4ajxz6pf3js2ngp-apple-framework-CoreFoundation-11.0.0","/nix/store/q57wwyd23i6msdawg6casxc3cgvxdsaa-apple-framework-Security-11.0.0","/nix/store/scz844iwh8nw8vcc2sr3n5vkkya24byi-apple-framework-CoreFoundation-11.0.0","/nix/store/z7n2ibxkj7fsggcfgy55mvzxwxsc2h96-apple-framework-ApplicationServices-11.0.0"],"registrationTime":1680294964,"signatures":["cache.nixos.org-1:e9RjAV+GzYP3hcmk/Pc91vs/6RyiKkhPkAoQJpoYtN+zQAp6+653bk+EGZdJKcvWh6FYznzKEsYW7tPe9S4oDQ=="]},{"deriver":"/nix/store/sfmalgqmpqqk4r43a4fivgr5xbng02nf-tzdata-2022g.drv","narHash":"sha256-wv+Ddg1yerUB6wptJdtH/lC2en+U7PF5olUMpnN+jIw=","narSize":2101576,"path":"/nix/store/fjjs1gcn8zy7pxcygi2pn9dkz06mycxf-tzdata-2022g","references":[],"registrationTime":1680294962,"signatures":["cache.nixos.org-1:/v+wzJcX12uaBbf3+03oMjU65GlYNq90cYHd2mSqsqKlhJmlyfDRHkYUYmnYOuKk11BjG2jI+jaY7OV901nWBQ=="]},{"deriver":"/nix/store/qvfmpjvxbb2375yiii4jw4rgbyw4rig8-apple-framework-CoreServices-11.0.0.drv","narHash":"sha256-2mYrTwKdEZ6x7LsF5/2Iec5NasAlZv1c2wzxuxzX+OQ=","narSize":3834768,"path":"/nix/store/fkd0lx594kgns98p53n6p3hj01isjsal-apple-framework-CoreServices-11.0.0","references":["/nix/store/2hl6k0mx0bl167kf4ggjaz0pcxbsz1bi-apple-framework-OpenDirectory-11.0.0","/nix/store/45dl40acmas1wkh2kva1db5bry1rwszs-apple-framework-CoreData-11.0.0","/nix/store/656hkg3497rx0wcssd895k4pvwc4jzag-apple-framework-CoreAudio-11.0.0","/nix/store/byzgp03rhiybvnr9n42i8b4931kaqchb-apple-framework-DiskArbitration-11.0.0","/nix/store/fkd0lx594kgns98p53n6p3hj01isjsal-apple-framework-CoreServices-11.0.0","/nix/store/hagvvvlp3cw03xx794sw3q4m2dz450zj-apple-framework-CFNetwork-11.0.0","/nix/store/hxrcjxyvcxzpd8k6v8fyqx6c9y89nkfk-apple-framework-NetFS-11.0.0","/nix/store/k7rk29pxrsy3ry29k4ajxz6pf3js2ngp-apple-framework-CoreFoundation-11.0.0","/nix/store/p0dyxxz38dr7iy9a9q71cxkahpdafz2a-apple-framework-ServiceManagement-11.0.0","/nix/store/q57wwyd23i6msdawg6casxc3cgvxdsaa-apple-framework-Security-11.0.0","/nix/store/scz844iwh8nw8vcc2sr3n5vkkya24byi-apple-framework-CoreFoundation-11.0.0"],"registrationTime":1680294962,"signatures":["cache.nixos.org-1:PRA5y4tzeff1ZDb48k1DeUOOW1H7UhWI1wfdWdxEMFdaUhLJ0jEmur6HAoA9atBSwOQUuwq/A4l0y9i1IwucAA=="]},{"deriver":"/nix/store/04qi86fi7klkw2d3gfsdfjlvka6g7za9-apple-framework-CoreBluetooth-11.0.0.drv","narHash":"sha256-HlsBhvGVdJsFClRUYKTcUKFVhcfnlMVs33jWHnWXLZI=","narSize":117616,"path":"/nix/store/g6h0632636m2r38hkm16gsl459hdlm4b-apple-framework-CoreBluetooth-11.0.0","references":[],"registrationTime":1680294962,"signatures":["cache.nixos.org-1:EiJ7cMaw8sCx2BYv5tigoQCLMkAysjcmc69E2QZQ7niXe2wwzSAj0VG34YrBmeY7HPBPqQafaLha8MQgLwHDBw=="]},{"deriver":"/nix/store/zhfbch65lwylpmjhhqn0s1qcw4mi2aga-libcxxabi-11.1.0.drv","narHash":"sha256-uJEjOASk971x6dFokiWrb3CVG5Y9yM3Q1l9E9H8nLFI=","narSize":647688,"path":"/nix/store/gs6iqzg3cswfsxwnwcvsnf6fn76wpbr2-libcxxabi-11.1.0","references":["/nix/store/gs6iqzg3cswfsxwnwcvsnf6fn76wpbr2-libcxxabi-11.1.0"],"registrationTime":1680294962,"signatures":["cache.nixos.org-1:jqHfRyhu8W2pDHInQ8G5TKeDEeqhznwNJVJCgUKwh+h2dDAgnWSVVhERQSMdTD8zR0WcoQHFicf5/JciiNPMCQ=="]},{"deriver":"/nix/store/3fjs7jkb6f7bfk9pm2p652zcms82062i-compiler-rt-libc-11.1.0.drv","narHash":"sha256-yBbNEpv0MsVa3yS9h96ChpjvF/augi4CMjERGVuDIhQ=","narSize":31360,"path":"/nix/store/h2hd63vq9js32ggqx9nvhsjqx4b2ckss-compiler-rt-libc-11.1.0-dev","references":["/nix/store/81jnzlpii91vz9y8rwb7g095zk2jj2qr-compiler-rt-libc-11.1.0"],"registrationTime":1680294962,"signatures":["cache.nixos.org-1:JFiCLxJpsuzqyVdpJqj6zvdHa0d2fiQFqDxTTA4IGVgbigf+y04m/LgxHoZsDyvYUaKcAq6V1FfRUPSrZK3+Bw=="]},{"deriver":"/nix/store/a1ryww6mn0m1avnisva3k4xd9y8q5n4w-unifdef-2.12.drv","narHash":"sha256-sHhXMmWDqjd/r+P5I+7D0dWhNgw8ukbD4tbnz+XNLdM=","narSize":81416,"path":"/nix/store/h4pzmkzjqwj7479wzcks7vjgq07grm0p-unifdef-2.12","references":["/nix/store/21fn46279sjpqqqqsy2cnkxr3rqzbwq6-apple-framework-CoreFoundation-11.0.0","/nix/store/ka6rabx4lz7m3habrjhh8hvbgxbz8r98-bash-5.2-p15"],"registrationTime":1680294963,"signatures":["cache.nixos.org-1:kM3Jk8lgQzyfAVEkSrdW/ueL/PtUq/GStFq4V8cMz4VJmXknxoxP5veEWK0m7m30LF9DTPUlH/DTqB5/zBBAAg=="]},{"deriver":"/nix/store/cs8cddmb9c13gppfvh6hkzkn5v9lnbgc-apple-framework-CFNetwork-11.0.0.drv","narHash":"sha256-CapHxRj+YhGyvtwroXr1TwA2pZ1ln6rcmgtJuQmKXeE=","narSize":199928,"path":"/nix/store/hagvvvlp3cw03xx794sw3q4m2dz450zj-apple-framework-CFNetwork-11.0.0","references":[],"registrationTime":1680294962,"signatures":["cache.nixos.org-1:gtOdI4kE+Z+UJWnnoHWnE3Jomx7edZgnOJ2FNMD5uZ25+ErlwovZWncieVQjNzg7szb/Ahr3e7OFsxsg7oApDg=="]},{"deriver":"/nix/store/3ikaism29xlnzjl2q46ipjlchbm7ayxl-gperf-3.1.drv","narHash":"sha256-YAoHzi5/Db9UYn7NVZ3ifFJmfrSEHk/Ugk1HcRDnlcY=","narSize":346424,"path":"/nix/store/hh91b8qb8ghv5jjpiyficqgd4rczfy5a-gperf-3.1","references":["/nix/store/21fn46279sjpqqqqsy2cnkxr3rqzbwq6-apple-framework-CoreFoundation-11.0.0","/nix/store/gs6iqzg3cswfsxwnwcvsnf6fn76wpbr2-libcxxabi-11.1.0","/nix/store/y56hqp2dgrh9hi9xyd88n0pcl6gn895d-libcxx-11.1.0"],"registrationTime":1680294963,"signatures":["cache.nixos.org-1:aOfBSCz1BwD3OZ1b7iAkYcKte5P52ygHbFQuQ0RWVH4h261o8N55Yk3Ute5is/8oEu+2IFY3WM/Ee2pAwp/1CQ=="]},{"deriver":"/nix/store/qxv7s1laqfpkh77mjqkdb5x8ph76l7mj-gettext-0.21.drv","narHash":"sha256-dJw9JdtPQO8DDAQ3vQoxNoH3b4wkhnllx97HOpfLXWY=","narSize":9361768,"path":"/nix/store/hr6cwwvfmfp3c131g1xqvjxp0d3cf4n5-gettext-0.21","references":["/nix/store/21fn46279sjpqqqqsy2cnkxr3rqzbwq6-apple-framework-CoreFoundation-11.0.0","/nix/store/gs6iqzg3cswfsxwnwcvsnf6fn76wpbr2-libcxxabi-11.1.0","/nix/store/hr6cwwvfmfp3c131g1xqvjxp0d3cf4n5-gettext-0.21","/nix/store/ka6rabx4lz7m3habrjhh8hvbgxbz8r98-bash-5.2-p15","/nix/store/lpv1rr58zr5xa2q60s1x4792pgb6w7fr-libiconv-50","/nix/store/y56hqp2dgrh9hi9xyd88n0pcl6gn895d-libcxx-11.1.0"],"registrationTime":1680294963,"signatures":["cache.nixos.org-1:c1k7xSG2zF/UY8zqm7XSH7QoL6v7neJsS1rrWGimJZ+MePDNUMWlrpcHYaSRcVZ+AiIiz1I7lOCkcXbR7U3SAg=="]},{"deriver":"/nix/store/01wgb0rzp9ns5ql714crjxhawza2syav-apple-framework-NetFS-11.0.0.drv","narHash":"sha256-7cY485HKJb5dxoASWhl3sj7keT0pefPL/2kZABvgDdc=","narSize":25272,"path":"/nix/store/hxrcjxyvcxzpd8k6v8fyqx6c9y89nkfk-apple-framework-NetFS-11.0.0","references":[],"registrationTime":1680294962,"signatures":["cache.nixos.org-1:aVDUjLcPontllIcl761jfvHG+T8l0VLY1WKzZwz7RlKcA9EXgsrsfQT2FBJfsOKF3oyqhrpzLqG1aTMPRsU2Cg=="]},{"deriver":"/nix/store/kq0arh7cx28y6i2knk1yiwr234razsr4-apple-framework-CoreLocation-11.0.0.drv","narHash":"sha256-I6BfHWUuuTyuTfVgUSMqB83irax4rr8wO1un531vpdw=","narSize":115504,"path":"/nix/store/hzjxfgdxac2bvmkw40bsyxm3kh73kl70-apple-framework-CoreLocation-11.0.0","references":[],"registrationTime":1680294962,"signatures":["cache.nixos.org-1:saNSCB0gJy0A2B0GE2a/QkMf46FVIhluV2sNPWJQqkYnhXe1BWh+cOquJsqGmqqnKo/jpnnCpuPigRBVfEeHCA=="]},{"deriver":"/nix/store/dsz76ww3f51mix8a18zkbpfsamx7xnyb-apple-framework-SecurityFoundation-11.0.0.drv","narHash":"sha256-OiZo2VhwptNILZn2gb1tSjx74u5EIyBqOm7xDXUPKj0=","narSize":20192,"path":"/nix/store/i62176fkd5yg2qbxrqb4gd1bbijzmh2m-apple-framework-SecurityFoundation-11.0.0","references":["/nix/store/q57wwyd23i6msdawg6casxc3cgvxdsaa-apple-framework-Security-11.0.0"],"registrationTime":1680294962,"signatures":["cache.nixos.org-1:gBmM2kaCBPiWQX/IwvWj+WVRXZn2GZpQifwo0H999JUhQVID8HdXQaeP+pf7Uxk4kf2DZQM5HHcGOnBn6mTEAw=="]},{"deriver":"/nix/store/yrpx8k6r05p18ar670ncrd6w2xaw2q65-llvm-11.1.0.drv","narHash":"sha256-2kWV/HrGEORX/yURU4HoaX4MiaRfpqh6ZcGOm6aRMxE=","narSize":40062480,"path":"/nix/store/imapnm2l9s2lsqi58qw3d2a2pxmlkq4d-llvm-11.1.0","references":["/nix/store/3bn6wnswkqmaiv0px062bblxbcqm5xhv-llvm-11.1.0-lib","/nix/store/3i2pp8m7cy8s2w23f9lzj4whl4grkpfg-zlib-1.2.13","/nix/store/ay0icgbhay57540wh6yy12sh5ngwgzy9-ncurses-6.4","/nix/store/gs6iqzg3cswfsxwnwcvsnf6fn76wpbr2-libcxxabi-11.1.0","/nix/store/lpv1rr58zr5xa2q60s1x4792pgb6w7fr-libiconv-50","/nix/store/y56hqp2dgrh9hi9xyd88n0pcl6gn895d-libcxx-11.1.0"],"registrationTime":1680294967,"signatures":["cache.nixos.org-1:ag9KTQ1OOsS8cenRqoQxeVDI0d87p7P+NCuqOOo4LGHOv630ck8uPnq+5c145ieBWcCF6XX0Y53nRkvpOk72Bw=="]},{"deriver":"/nix/store/yg9dx2mqhlrgzskydyxj2nnx3rfkc54w-binutils-2.40.drv","narHash":"sha256-ZI7Ckylg2nQcCRY/Aq2HNG5N0yy7ZCLTb786A9R1X0w=","narSize":10047288,"path":"/nix/store/jq929fndq5k44kc8pn888jljrpsj553w-binutils-2.40","references":["/nix/store/3i2pp8m7cy8s2w23f9lzj4whl4grkpfg-zlib-1.2.13","/nix/store/41c100x9gdslcrcpz5gq0pjgy8a7klhs-gettext-0.21","/nix/store/jq929fndq5k44kc8pn888jljrpsj553w-binutils-2.40","/nix/store/riw9p427ld356bpa5xi121jlg1q9qpqb-binutils-2.40-lib"],"registrationTime":1680294964,"signatures":["cache.nixos.org-1:5iX+Qp3cOKFuzGg5p04pIHa/L/mZ53uSKiDbNlZKWcphQSUjQ65kSdVF/QBn04XNTdm43Q+/8O+wfZup4lX2Ag=="]},{"deriver":"/nix/store/sb5miilhi7hrbrgfllvk07w1ycfx9qka-libtapi-1100.0.11.drv","narHash":"sha256-fdkNGYCn8LoUOEqip6ZyeDsBvqSGSBwvAhWNjSCPSpY=","narSize":25907128,"path":"/nix/store/jx8f6kj7pv57x54zihb3wp3xglbwq5za-libtapi-1100.0.11","references":["/nix/store/ay0icgbhay57540wh6yy12sh5ngwgzy9-ncurses-6.4","/nix/store/gs6iqzg3cswfsxwnwcvsnf6fn76wpbr2-libcxxabi-11.1.0","/nix/store/jx8f6kj7pv57x54zihb3wp3xglbwq5za-libtapi-1100.0.11","/nix/store/y56hqp2dgrh9hi9xyd88n0pcl6gn895d-libcxx-11.1.0"],"registrationTime":1680294963,"signatures":["cache.nixos.org-1:dEURJniej5uXATu57GqkFphaS/f0uNHZ40BBXn45T+wNnaF716L+6ZXEj2yMe1733uBMh4Wh39rQpv5LHADPDQ=="]},{"deriver":"/nix/store/k8wkngap97l5i46r0ik8k70s9vzypxpv-apple-framework-CoreFoundation-11.0.0.drv","narHash":"sha256-5wwSc9mtU9PQr/Abx7oOsMfKcqGL/hBduYlQPi/N68Y=","narSize":695784,"path":"/nix/store/k7rk29pxrsy3ry29k4ajxz6pf3js2ngp-apple-framework-CoreFoundation-11.0.0","references":["/nix/store/3pd5fbv4did9ya9mjbcj25rfmdk021sm-libobjc-11.0.0"],"registrationTime":1680294962,"signatures":["cache.nixos.org-1:LChcLky7dgGZ2RLaHEj6zhenJb9It5eDW6QaNK7YP+nmvUBOYhJokmM/wOILyjvHBCpWAEheBCPVGqOVWeynBQ=="]},{"deriver":"/nix/store/xf5bp0236i96h8098viqf718y0qw97nq-bash-5.2-p15.drv","narHash":"sha256-UpQeK2+jDcXpGAzcxQdC+8G58PGKe5NPVX7zu7/TDvE=","narSize":3329712,"path":"/nix/store/ka6rabx4lz7m3habrjhh8hvbgxbz8r98-bash-5.2-p15","references":["/nix/store/ka6rabx4lz7m3habrjhh8hvbgxbz8r98-bash-5.2-p15"],"registrationTime":1680294962,"signatures":["cache.nixos.org-1:4Dv3LMyMYRx/6kfgNQbQkt8bWkdwFKMYZA6i+r6+YKAjToKv4f4zrLF5oS7mhSzSzi5AXGqCgEF1EuMWC7eNCw=="]},{"deriver":"/nix/store/zq1jrm3448l4i13q0imi9b891x07b6bp-sigtool-0.1.3.drv","narHash":"sha256-i4x70mjjX02V/BtWubdMbnNXKtTVuesi20lC7KBVvwg=","narSize":859320,"path":"/nix/store/kg0wv92mmn7bbyrc0hfg4k8mr5z4pn6r-sigtool-0.1.3","references":["/nix/store/ay24bgl7phsf067kfnvg5czbqq7gl6n0-openssl-3.0.8","/nix/store/gs6iqzg3cswfsxwnwcvsnf6fn76wpbr2-libcxxabi-11.1.0","/nix/store/y56hqp2dgrh9hi9xyd88n0pcl6gn895d-libcxx-11.1.0"],"registrationTime":1680294963,"signatures":["cache.nixos.org-1:lgJt1U6KQC1hXYRDBTtHLWtKOhpQVyviZLaT3z4KnfBwMxa/RgJ9A6OWImo6/i4YRpm8i32s0KvetoHzg2lqBQ=="]},{"deriver":"/nix/store/h86qc3sbb2rn4znpy55mczhv2xwv0iz2-libxml2-2.10.3.drv","narHash":"sha256-ZhM5f+SIcUnEviwBilW1U824OoUrLG3Bjvt+oqK+B5s=","narSize":1315144,"path":"/nix/store/la358rlj7ck6z9i4cwwvla05zmdrrp42-libxml2-2.10.3","references":["/nix/store/21fn46279sjpqqqqsy2cnkxr3rqzbwq6-apple-framework-CoreFoundation-11.0.0","/nix/store/3i2pp8m7cy8s2w23f9lzj4whl4grkpfg-zlib-1.2.13","/nix/store/la358rlj7ck6z9i4cwwvla05zmdrrp42-libxml2-2.10.3","/nix/store/lpv1rr58zr5xa2q60s1x4792pgb6w7fr-libiconv-50"],"registrationTime":1680294963,"signatures":["cache.nixos.org-1:ZIJJnxm7onU3F4zbCN5HEktYTo3QBE/e5t+9PaVt9CwwhdINxyteQDFLz01ANRnBj5XQBeKuwg2O/xWGPAplDA=="]},{"deriver":"/nix/store/cibkzpwqlf16if7s1pkmh01a7ynscdk2-libiconv-50.drv","narHash":"sha256-XuU7UtRLuB1Q7LCIaZwDilYUdSi+2CWlKIc1b0M2eJ0=","narSize":2264808,"path":"/nix/store/lpv1rr58zr5xa2q60s1x4792pgb6w7fr-libiconv-50","references":["/nix/store/lpv1rr58zr5xa2q60s1x4792pgb6w7fr-libiconv-50"],"registrationTime":1680294962,"signatures":["cache.nixos.org-1:A91GmaWI5lA+qt0yHzRrMKldBtbBI6GfIDbD/DpZpmMemb5dUXY/hQxhLes5n+Ne62593YSbZoa56339rqWzDA=="]},{"deriver":"/nix/store/lplhlpsi1ch93marxj83xvsglwg9ffqh-Platforms.drv","narHash":"sha256-FQTICb/NLjbHixpVS57yTyDXeo4T1DxzmFHZso9YEEI=","narSize":17440,"path":"/nix/store/lqz6zpi4qzpgrqwwxbd1bhmac3cf7nbn-Platforms","references":["/nix/store/6jdcd2hw2jc99iy72zb80si1q41l008h-SDKs","/nix/store/lqz6zpi4qzpgrqwwxbd1bhmac3cf7nbn-Platforms"],"registrationTime":1680294962,"signatures":["cache.nixos.org-1:swfVfXP6o8TNxrByip6lBZpDcXkJ0E0bR+2rE9bsoRQ/WT9oqv+JlkulqPcZf8E4eXIcipmA3rYq85jM3/x+AQ=="]},{"deriver":"/nix/store/8gxy2s8f2wv53by7siknbikvr8n1q61s-go-1.20.2.drv","narHash":"sha256-kI8ZrhQjPpLxX7EgCLZgf4nfNLAyflj9frXs77b/OBk=","narSize":238591992,"path":"/nix/store/mil5crms7gfpv03vjj094zz1igvapv6i-go-1.20.2","references":["/nix/store/7l1jnzfkx3g4idjyi7vn25rmszl8lizz-iana-etc-20221107","/nix/store/f7p955pdwhgirijk33mash7rmlkzklkx-apple-framework-Foundation-11.0.0","/nix/store/fjjs1gcn8zy7pxcygi2pn9dkz06mycxf-tzdata-2022g","/nix/store/ka6rabx4lz7m3habrjhh8hvbgxbz8r98-bash-5.2-p15","/nix/store/mil5crms7gfpv03vjj094zz1igvapv6i-go-1.20.2","/nix/store/pjcnn0b7xmkmgngxlp7zc669gf5kcnyj-mailcap-2.1.53","/nix/store/q57wwyd23i6msdawg6casxc3cgvxdsaa-apple-framework-Security-11.0.0","/nix/store/y62l0hi83wq2wbs5wmvcckrd947alf7p-xcodebuild-0.1.2-pre"],"registrationTime":1680294973,"signatures":["cache.nixos.org-1:dblTdSQbWJFC9kdueov+CKnOBU2QNS/4EHcWKAmLZ2p3mDqLTIyN/fttK3waorSawcC6vozeQR9+AXw4pY40Bw=="]},{"deriver":"/nix/store/l29qg49s13ygvri8s23plgy0rjgqyy6f-apple-framework-IOBluetooth-11.0.0.drv","narHash":"sha256-PvPieisma7wnHQDRGoCmGWvWKDIiaQK6vHMJWvvwb+M=","narSize":572304,"path":"/nix/store/n8bxvnxzfwv5qi55m8arqmq6i6ih3bcf-apple-framework-IOBluetooth-11.0.0","references":["/nix/store/5ncnv0kgz3fzf3bnq5bl35lw18yvw706-apple-framework-IOKit-11.0.0","/nix/store/g6h0632636m2r38hkm16gsl459hdlm4b-apple-framework-CoreBluetooth-11.0.0"],"registrationTime":1680294962,"signatures":["cache.nixos.org-1:uhSHJd+tXhy1RPPbraUj15W9jAonz6iUoc4tqDVELbaSa0iwfQUIJGZga244XY4Yzzyd9F9d1kIAldkfr9aQCQ=="]},{"deriver":"/nix/store/izbp9djl97g3kf9izmwkjmpi4yk65xmw-cctools-binutils-darwin-973.0.1.drv","narHash":"sha256-YLpCnrgHu23jr9SizizZ9tFATk30zvigSgA3LdoFgpo=","narSize":4776,"path":"/nix/store/nq3fp1bjaizwxv6j7gqnza6s8sdiamm8-cctools-binutils-darwin-973.0.1","references":["/nix/store/35vkq2dc6v7zpj83zxavc5x78wpz5ak3-cctools-port-973.0.1","/nix/store/imapnm2l9s2lsqi58qw3d2a2pxmlkq4d-llvm-11.1.0","/nix/store/jq929fndq5k44kc8pn888jljrpsj553w-binutils-2.40","/nix/store/ka6rabx4lz7m3habrjhh8hvbgxbz8r98-bash-5.2-p15","/nix/store/nyrn55869kmx1bn1rx4mhk18irypbh1r-clang-11.1.0"],"registrationTime":1680294968,"signatures":["cache.nixos.org-1:x33WeBvxb1bHPonVbhK2MTlHq9v4Krffi2GDo0Jp9oHWT4zC9ysMlL6wDXpAi5wc6G2O/EqU7b8xJZgcbUwkBA=="]},{"deriver":"/nix/store/qnzsac455zcx7l8qbq5fdlkizgnqrh72-apple-framework-CoreText-11.0.0.drv","narHash":"sha256-OTN3lCzdemlmIz/uWK16TjI4rSO+ExllHyTe4N5dyg4=","narSize":431240,"path":"/nix/store/nrlivsnsj6b90frl21gsbxc72fc8j6h9-apple-framework-CoreText-11.0.0","references":["/nix/store/4al6fm8vlmv3v7749sfqvj34vnhsfbsq-apple-framework-CoreGraphics-11.0.0"],"registrationTime":1680294963,"signatures":["cache.nixos.org-1:VpqjPgJg+g4gW+6W26hEawx+58LJyRhYwpJhnr9p5yN+5K/yrXiejiKs89MF7ioMV5OmrT+gzdJEGSK7dnhECg=="]},{"deriver":"/nix/store/k9kkb1cp9fa9b49kj6bp328vg9dpv5z8-clang-11.1.0.drv","narHash":"sha256-ZBFA1K67A2LCx4ZoIoUvVAA3GgJkhth3JdxV2ay3Wy4=","narSize":24255736,"path":"/nix/store/nyrn55869kmx1bn1rx4mhk18irypbh1r-clang-11.1.0","references":["/nix/store/3bn6wnswkqmaiv0px062bblxbcqm5xhv-llvm-11.1.0-lib","/nix/store/gs6iqzg3cswfsxwnwcvsnf6fn76wpbr2-libcxxabi-11.1.0","/nix/store/lpv1rr58zr5xa2q60s1x4792pgb6w7fr-libiconv-50","/nix/store/v1vhsx3ss1ap9nn520y9dmibhqjpnf91-clang-11.1.0-lib","/nix/store/y56hqp2dgrh9hi9xyd88n0pcl6gn895d-libcxx-11.1.0"],"registrationTime":1680294968,"signatures":["cache.nixos.org-1:jmYEegYbOQMb6iIgZY/NswKsZgrTaJZgOKyMkFtHUjhOwfvyejLe92p3un0pB4fJNo9KItXZK8uUCNDhjxxUAQ=="]},{"deriver":"/nix/store/1d9x415bv31961843i5182b5pbmzj9gi-apple-framework-ServiceManagement-11.0.0.drv","narHash":"sha256-pl5NjbxrKjQc/SP9apqDmsn69EKn+DBMXLcEHmSLR10=","narSize":17224,"path":"/nix/store/p0dyxxz38dr7iy9a9q71cxkahpdafz2a-apple-framework-ServiceManagement-11.0.0","references":["/nix/store/q57wwyd23i6msdawg6casxc3cgvxdsaa-apple-framework-Security-11.0.0"],"registrationTime":1680294962,"signatures":["cache.nixos.org-1:PGqg2JyFQEczKatl0iZtMzVVOlD6aZPQpLVw6qW6aniBc35gFS3PgC7FKqMevHmjenltfNO8gYsWk3DS9kv6AQ=="]},{"deriver":"/nix/store/g4v5v0apf0x64g1rj5vrs6vzcvb1g9y9-mailcap-2.1.53.drv","narHash":"sha256-VCCHYUJVISf3h7MGrOQDZQJYnKjq/fXkTZWmnVE3P7M=","narSize":112040,"path":"/nix/store/pjcnn0b7xmkmgngxlp7zc669gf5kcnyj-mailcap-2.1.53","references":[],"registrationTime":1680294962,"signatures":["cache.nixos.org-1:7i94KwnXLxGVzXzFBM93bHJbnDHpvZLaVm14VtFtNIWsqMYzt/o+Zfyu2XAS4Bk/K1zh6cdwM0mW2mT8CgmsDg=="]},{"deriver":"/nix/store/y4vwc2nqv1airr8h8wwzl8ad9hghvxpb-apple-framework-Security-11.0.0.drv","narHash":"sha256-4EkMWIn0aqrgJG6r0O/i1nxI2Nmb+LOTDyKI1BwK+w0=","narSize":1539168,"path":"/nix/store/q57wwyd23i6msdawg6casxc3cgvxdsaa-apple-framework-Security-11.0.0","references":["/nix/store/5ncnv0kgz3fzf3bnq5bl35lw18yvw706-apple-framework-IOKit-11.0.0","/nix/store/7p8qgl96a2grvwaaxnpw678m4f99l86v-apple-lib-libDER"],"registrationTime":1680294962,"signatures":["cache.nixos.org-1:dXTqHJ+gEVg5TrNPr/6srunnwCporxNxa5WPFbHICd4fsVgQnEQQSD6ZCARm4ERRRWIPR5xKd0rxLn1rFeZ3Aw=="]},{"deriver":"/nix/store/s2m6j3lx4a3qvscq9ybd82q9a60n0j43-pcre-8.45.drv","narHash":"sha256-YvZib1zDVrhTB8xrzaPGA+BgZXBsn0xpRAn6Xeod0Dk=","narSize":375792,"path":"/nix/store/qdk9ccf3112ppwynzfdv20aiz71lxqjl-pcre-8.45","references":["/nix/store/qdk9ccf3112ppwynzfdv20aiz71lxqjl-pcre-8.45"],"registrationTime":1680294962,"signatures":["cache.nixos.org-1:9v6JW4JbYCyYhXdRvik3CaQxC40IhfDYpU2Ooq6VVJhzR33UW7idKW06/9yTTyrji87UHcw+vivRsvxi3cBCAA=="]},{"deriver":"/nix/store/cbrj7mmjl93gjg45qbs41781irnfi471-apple-framework-ColorSync-11.0.0.drv","narHash":"sha256-FA36B7MRkFTccKcLxKP0Z1SiocQfDqLJ1e0O5Wcw74c=","narSize":143656,"path":"/nix/store/qjy23mrisdwl3khmkz55mvinwjxmi7vr-apple-framework-ColorSync-11.0.0","references":[],"registrationTime":1680294962,"signatures":["cache.nixos.org-1:i6Hax2DgFHHBB6mziX3Oyc8lPwH4iz2OytMRdNA17fobvrRbltGWLWGzC44FcfvdlgDNaCXX9OS1/QbQdXfPAQ=="]},{"deriver":"/nix/store/ii093lv32w6v872n8vnws64wd2bqy3zz-coreutils-9.1.drv","narHash":"sha256-ZXBYp2RnYzOy/UbTzCE2i8/l59TcSqp1MYRHJEqwmiU=","narSize":1413672,"path":"/nix/store/qr4zarpiaf3fk2bm01gnb33grg2nwwrh-coreutils-9.1","references":["/nix/store/qr4zarpiaf3fk2bm01gnb33grg2nwwrh-coreutils-9.1","/nix/store/s8ddra36d0p31gxbg5qfahdnd0hy58ca-gmp-with-cxx-6.2.1"],"registrationTime":1680294962,"signatures":["cache.nixos.org-1:y2LixCK0BPeZN4t3Bwbhgz/gNp61PQOnVe9b8d6k3xk1YyQrZxHLOohw4LTLAxPvMxJl/I7LgzRSZzedzZ0MBw=="]},{"deriver":"/nix/store/yg9dx2mqhlrgzskydyxj2nnx3rfkc54w-binutils-2.40.drv","narHash":"sha256-gErDRvxRh4ZXV/QEPZzk5BK4Ipkmbytfrx9v3CJ+kfY=","narSize":3032488,"path":"/nix/store/riw9p427ld356bpa5xi121jlg1q9qpqb-binutils-2.40-lib","references":["/nix/store/3i2pp8m7cy8s2w23f9lzj4whl4grkpfg-zlib-1.2.13","/nix/store/41c100x9gdslcrcpz5gq0pjgy8a7klhs-gettext-0.21","/nix/store/riw9p427ld356bpa5xi121jlg1q9qpqb-binutils-2.40-lib"],"registrationTime":1680294963,"signatures":["cache.nixos.org-1:XiPYUg+dFVh0DKze9bEiv5M7TGnb+J7zCS9bgqHxQ2n3UYC7xzgoH0+r5cu+SRdqI5WeqFGpam+ybONX1ItUDw=="]},{"deriver":"/nix/store/7cjvfzzgxrxfs7369slhhh9pxyakk2p9-gmp-with-cxx-6.2.1.drv","narHash":"sha256-MbgjK5DhEBqhjQU2Q9rmT+YFnR0LTfFUsz8j7gb1Qqc=","narSize":584344,"path":"/nix/store/s8ddra36d0p31gxbg5qfahdnd0hy58ca-gmp-with-cxx-6.2.1","references":["/nix/store/gs6iqzg3cswfsxwnwcvsnf6fn76wpbr2-libcxxabi-11.1.0","/nix/store/s8ddra36d0p31gxbg5qfahdnd0hy58ca-gmp-with-cxx-6.2.1","/nix/store/y56hqp2dgrh9hi9xyd88n0pcl6gn895d-libcxx-11.1.0"],"registrationTime":1680294962,"signatures":["cache.nixos.org-1:S2cIV4cjHLtmruL9Z2vTCFA9T5vusxy23LWPrefLljiMZT2wEepC+MYzfi2xRLEySgVlnpvo6jt0QmGgEkUvBQ=="]},{"deriver":"/nix/store/dv32pmfnrrgy0bxmckmpgss14zpib50v-apple-framework-CoreFoundation-11.0.0.drv","narHash":"sha256-q5/NpmDY1BqhAxccHWi4jtDGlkVr7tXmYoULglHDVxI=","narSize":696408,"path":"/nix/store/scz844iwh8nw8vcc2sr3n5vkkya24byi-apple-framework-CoreFoundation-11.0.0","references":["/nix/store/3pd5fbv4did9ya9mjbcj25rfmdk021sm-libobjc-11.0.0","/nix/store/scz844iwh8nw8vcc2sr3n5vkkya24byi-apple-framework-CoreFoundation-11.0.0"],"registrationTime":1680294962,"signatures":["cache.nixos.org-1:LJ5QvOi6rJl3adnu61SqUeEowuk52m9PMTEuqj9QChBiLNz2ffIKXutfIuWeiK7fU6OGTnTm30pOjW3lV4+CAw=="]},{"deriver":"/nix/store/axsh3wg79wbfwc911rg6p0hq22zxamk7-apple-framework-IOSurface-11.0.0.drv","narHash":"sha256-vsvS4kh5G0il7Df/iqmTvzzoktUP8p/iHqPtX1L3siU=","narSize":59560,"path":"/nix/store/sllbiczqxnlg732nj0sn21b1rgsac5mg-apple-framework-IOSurface-11.0.0","references":["/nix/store/5ncnv0kgz3fzf3bnq5bl35lw18yvw706-apple-framework-IOKit-11.0.0"],"registrationTime":1680294962,"signatures":["cache.nixos.org-1:7MYr6a4tDX9T46nw8+uCundbNqlS3tQQi3ZACjtzhizNfp0cZ2S7fpLmEjfQt/4AYnvbkY3FbJfpRwbRhzNmDQ=="]},{"deriver":"/nix/store/xmph46s6jbi59j5ibrzcah9i4zzwp7mv-apple-framework-ImageIO-11.0.0.drv","narHash":"sha256-SSw9QfqysB7AzV2hlV4flF54vO+EfT0qnGkmaJ/AWck=","narSize":206368,"path":"/nix/store/swhsihvli1wfa0xm975vzx7diz3kfv34-apple-framework-ImageIO-11.0.0","references":["/nix/store/4al6fm8vlmv3v7749sfqvj34vnhsfbsq-apple-framework-CoreGraphics-11.0.0"],"registrationTime":1680294963,"signatures":["cache.nixos.org-1:xg10e9CcFVLm2r2rs12KAH9/mu5ICoedqMU6MfAoX/ZWi8NHOb+QxWnUPxte0psHB2WRsIc24t2KagRSjlANBg=="]},{"deriver":"/nix/store/7fxn3davv0lqy3jk0skasanlwkh2q4i8-apple-framework-Accelerate-11.0.0.drv","narHash":"sha256-XoP77IvsyFgM/jHDoNaIrD8qUMrM4RZMwEoJTcF0qSM=","narSize":9997680,"path":"/nix/store/sx4lwrgyay49ynkpp2pq71s0l40mj1f9-apple-framework-Accelerate-11.0.0","references":["/nix/store/d6q9q9gqrpbl799j70jk0rp3sff2andd-apple-framework-CoreWLAN-11.0.0","/nix/store/n8bxvnxzfwv5qi55m8arqmq6i6ih3bcf-apple-framework-IOBluetooth-11.0.0","/nix/store/sx4lwrgyay49ynkpp2pq71s0l40mj1f9-apple-framework-Accelerate-11.0.0"],"registrationTime":1680294962,"signatures":["cache.nixos.org-1:XJVxnIyVAjALAgfSjw+Tah2tet0tm+GZoqzLNbxlFT1NJHu5g6Tw4YGLLX1wgLUhwLYqal/vz6/R0GeqSaYeBA=="]},{"deriver":"/nix/store/k9kkb1cp9fa9b49kj6bp328vg9dpv5z8-clang-11.1.0.drv","narHash":"sha256-aykYP1ci8shzDZ+kV9ZQC3teO+oi80Cq9vJxtGKlp9Q=","narSize":177009208,"path":"/nix/store/v1vhsx3ss1ap9nn520y9dmibhqjpnf91-clang-11.1.0-lib","references":["/nix/store/3bn6wnswkqmaiv0px062bblxbcqm5xhv-llvm-11.1.0-lib","/nix/store/gs6iqzg3cswfsxwnwcvsnf6fn76wpbr2-libcxxabi-11.1.0","/nix/store/lpv1rr58zr5xa2q60s1x4792pgb6w7fr-libiconv-50","/nix/store/v1vhsx3ss1ap9nn520y9dmibhqjpnf91-clang-11.1.0-lib","/nix/store/y56hqp2dgrh9hi9xyd88n0pcl6gn895d-libcxx-11.1.0"],"registrationTime":1680294968,"signatures":["cache.nixos.org-1:aMTuLym4hxss2NpTE1iucWrfvsc6wNKd0XOQRRD/7GaVQ784D3xsFa/vhiOtET9PiBMXeeINSKM8HsJpUtcPCQ=="]},{"deriver":"/nix/store/2ij7j3gjlm583p74nipyyigc216v7qjw-cctools-binutils-darwin-wrapper-973.0.1.drv","narHash":"sha256-tyyPjX6xbI//coS5QoeOsjREz88MORv4jFbnkyJBJmk=","narSize":36216,"path":"/nix/store/v3ks390idh1xqnh3r0zlkqc86pcy7lva-cctools-binutils-darwin-wrapper-973.0.1","references":["/nix/store/0vmh9wsqldbhc4m0fzlmri6jymn1y1y5-expand-response-params","/nix/store/0xjfx5w9xhnsx2hharl3mpdmrf07q9fm-libSystem-11.0.0","/nix/store/d6lbh51mr15c99h0vjcvh98ibnd8k21z-signing-utils","/nix/store/ka6rabx4lz7m3habrjhh8hvbgxbz8r98-bash-5.2-p15","/nix/store/nq3fp1bjaizwxv6j7gqnza6s8sdiamm8-cctools-binutils-darwin-973.0.1","/nix/store/qr4zarpiaf3fk2bm01gnb33grg2nwwrh-coreutils-9.1","/nix/store/v3ks390idh1xqnh3r0zlkqc86pcy7lva-cctools-binutils-darwin-wrapper-973.0.1","/nix/store/va24pgycrq0bkv1m9097gfz5h2cvhaln-post-link-sign-hook"],"registrationTime":1680294968,"signatures":["cache.nixos.org-1:ZkRJwI5dJzzD3bLLiVfjJI5M7cI+7qvRd+6b/qa4TpOMo8hX6wcqJ4M47iwGNH4mWpQS+oVtSQrGTuyYYdCQBQ=="]},{"deriver":"/nix/store/m0v888j3ydfva9jxhvrkkx2fiqcaa0q7-post-link-sign-hook.drv","narHash":"sha256-SMa3qPMXct/UMff8V10o6Rg5SL/aSSc9yGeKvoqexK8=","narSize":336,"path":"/nix/store/va24pgycrq0bkv1m9097gfz5h2cvhaln-post-link-sign-hook","references":["/nix/store/kg0wv92mmn7bbyrc0hfg4k8mr5z4pn6r-sigtool-0.1.3"],"registrationTime":1680294963,"signatures":["cache.nixos.org-1:rUjJCHVkqvbzWEU2wlUVukHxA+q9I/JlI4bagav3QfsGk65SgVFA81T5Axvr63SYhoaLwTIAy0LCU89Zf4FRCA=="]},{"deriver":"/nix/store/q371wypg6m0hl67zyw7r11spy9ch1kga-libffi-3.4.4.drv","narHash":"sha256-+1NSXnQRxqh5MuL6bNJ+0Gmqq/y+hH3ubYhxXVhQpD4=","narSize":122488,"path":"/nix/store/xc88ky1w798ncij3jr12gqcv2w9njyy2-libffi-3.4.4","references":["/nix/store/xc88ky1w798ncij3jr12gqcv2w9njyy2-libffi-3.4.4"],"registrationTime":1680294962,"signatures":["cache.nixos.org-1:EWNqP4t0NCaJ5LuLSKL6QxwWIH9AvsWL0JMai5SqM+yA7rKWaF111AYwsP5cykuznNOVhO5UFGz9xmfNOom0Cw=="]},{"deriver":"/nix/store/l7vnjw6d6ms6inhbyx4an5b45lc80bxb-nix.xcconfig.drv","narHash":"sha256-wOdsVLriiXBFRwRodYTWNqqevkKT9TSMUjEwdOuQyuc=","narSize":136,"path":"/nix/store/xwlq8b7swd014irh2qlvmqnas35iry6g-nix.xcconfig","references":[],"registrationTime":1680294962,"signatures":["cache.nixos.org-1:U3KzYH4ejSUy54B4GvJj/cykYKqOm4AMY3QUbn9RTAP6y4rzsWX/lfw4whGmUMhy883w8EQAlCPW0r9rtepoBQ=="]},{"deriver":"/nix/store/1xp025l3g3waa0qai7qcfgm4cbc6np9c-libcxx-11.1.0.drv","narHash":"sha256-7o2RM5wAc3W6/XRx4iOBKZZT4/DDLjpXZHm3V5BB+5E=","narSize":1989688,"path":"/nix/store/y56hqp2dgrh9hi9xyd88n0pcl6gn895d-libcxx-11.1.0","references":["/nix/store/gs6iqzg3cswfsxwnwcvsnf6fn76wpbr2-libcxxabi-11.1.0","/nix/store/y56hqp2dgrh9hi9xyd88n0pcl6gn895d-libcxx-11.1.0"],"registrationTime":1680294962,"signatures":["cache.nixos.org-1:JRRpVeIbHIIW+wGKLU5HAzbn4BLG7OG7u2Th7IYgcQUDhCE806EDawMT079LHh5IAfoz0Qn6Z0tuxWEwaH9iCA=="]},{"deriver":"/nix/store/sicznvv00s32rqqcaksqljvsqdjnsxdr-xcodebuild-0.1.2-pre.drv","narHash":"sha256-m8QAXXxdZbO5PH5nl1gmfrELLKoEUbXkKpihUbt+jJA=","narSize":9528,"path":"/nix/store/y62l0hi83wq2wbs5wmvcckrd947alf7p-xcodebuild-0.1.2-pre","references":["/nix/store/6jdcd2hw2jc99iy72zb80si1q41l008h-SDKs","/nix/store/a8kldaazsbvv07gjpx0nr9bqx3q9fk47-xcbuild-0.1.2-pre","/nix/store/d6fa6g9i8x410kankvzvmzrq90pb82qh-Toolchains","/nix/store/ka6rabx4lz7m3habrjhh8hvbgxbz8r98-bash-5.2-p15","/nix/store/lqz6zpi4qzpgrqwwxbd1bhmac3cf7nbn-Platforms","/nix/store/xwlq8b7swd014irh2qlvmqnas35iry6g-nix.xcconfig","/nix/store/y62l0hi83wq2wbs5wmvcckrd947alf7p-xcodebuild-0.1.2-pre"],"registrationTime":1680294968,"signatures":["cache.nixos.org-1:8lVCzctpBZW+OCEnOkWWU7FsvnnHJeMeFE1S3JFJhKlDohi06d9gTbt2vNA4z9yGnLQUYOmqXKOpnRYz+i8aDw=="]},{"deriver":"/nix/store/0vmcf155c3iyf0ss2ibfcjv4zxh537lq-apple-framework-ApplicationServices-11.0.0.drv","narHash":"sha256-JtUuERDDkBg6Ffq/EUw8qbnWt/UGm+CVsvhnveM67Mo=","narSize":1242880,"path":"/nix/store/z7n2ibxkj7fsggcfgy55mvzxwxsc2h96-apple-framework-ApplicationServices-11.0.0","references":["/nix/store/4al6fm8vlmv3v7749sfqvj34vnhsfbsq-apple-framework-CoreGraphics-11.0.0","/nix/store/fkd0lx594kgns98p53n6p3hj01isjsal-apple-framework-CoreServices-11.0.0","/nix/store/nrlivsnsj6b90frl21gsbxc72fc8j6h9-apple-framework-CoreText-11.0.0","/nix/store/qjy23mrisdwl3khmkz55mvinwjxmi7vr-apple-framework-ColorSync-11.0.0","/nix/store/swhsihvli1wfa0xm975vzx7diz3kfv34-apple-framework-ImageIO-11.0.0","/nix/store/z7n2ibxkj7fsggcfgy55mvzxwxsc2h96-apple-framework-ApplicationServices-11.0.0"],"registrationTime":1680294963,"signatures":["cache.nixos.org-1:jwThFy+SelmeNuEj2s4YrqEpVRLYgg9E4s0YhqhjMcxv22bS7tIsHk/ISg1Lvw0h93oEF2wM2bl3y2mqJrnvDQ=="]},{"deriver":"/nix/store/imjvmmr1hfah5mvvb5m9lmi4kl8233m0-gnugrep-3.7.drv","narHash":"sha256-imXy9d4NQaN0UDycvqxp1UkPCN7YunVTlfcO5KdzOdw=","narSize":306864,"path":"/nix/store/zmfr0yl6sab6q9rkd0shrpviq0f4i7mi-gnugrep-3.7","references":["/nix/store/ka6rabx4lz7m3habrjhh8hvbgxbz8r98-bash-5.2-p15","/nix/store/lpv1rr58zr5xa2q60s1x4792pgb6w7fr-libiconv-50","/nix/store/qdk9ccf3112ppwynzfdv20aiz71lxqjl-pcre-8.45","/nix/store/zmfr0yl6sab6q9rkd0shrpviq0f4i7mi-gnugrep-3.7"],"registrationTime":1680294963,"signatures":["cache.nixos.org-1:XoMaKds2W4irlCnl3zLRP+q0mKEv1T5bFqTQh0zEmi7bK5T2EwjlIFXj5AtoflQnpi2yKxR5w1iEUWMqrt2YBA=="]}] \ No newline at end of file diff --git a/internal/shenv/README.md b/internal/shenv/README.md deleted file mode 100644 index 3ed1ed5986e..00000000000 --- a/internal/shenv/README.md +++ /dev/null @@ -1,8 +0,0 @@ -Code in this directory was copy-pasted from the direnv codebase: -https://github.com/direnv/direnv/blob/master/internal/cmd/ - -We could not directly import this code because in the direnv -code it is inside an `internal` directory, and hence not -exported. - -Full credit to the direnv authors. diff --git a/internal/shenv/shell_bash.go b/internal/shenv/shell_bash.go deleted file mode 100644 index 295f08052b4..00000000000 --- a/internal/shenv/shell_bash.go +++ /dev/null @@ -1,181 +0,0 @@ -package shenv - -import "fmt" - -type bash struct{} - -// Bash shell instance -var Bash Shell = bash{} - -const bashHook = ` -_devbox_hook() { - local previous_exit_status=$?; - trap -- '' SIGINT; - eval "$(devbox shellenv --config {{ .ProjectDir }})"; - trap - SIGINT; - return $previous_exit_status; -}; -if ! [[ "${PROMPT_COMMAND:-}" =~ _devbox_hook ]]; then - PROMPT_COMMAND="_devbox_hook${PROMPT_COMMAND:+;$PROMPT_COMMAND}" -fi -` - -func (sh bash) Hook() (string, error) { - return bashHook, nil -} - -func (sh bash) Export(e ShellExport) (out string) { - for key, value := range e { - if value == nil { - out += sh.unset(key) - } else { - out += sh.export(key, *value) - } - } - return out -} - -func (sh bash) Dump(env Env) (out string) { - for key, value := range env { - out += sh.export(key, value) - } - return out -} - -func (sh bash) export(key, value string) string { - return "export " + sh.escape(key) + "=" + sh.escape(value) + ";" -} - -func (sh bash) unset(key string) string { - return "unset " + sh.escape(key) + ";" -} - -func (sh bash) escape(str string) string { - return BashEscape(str) -} - -/* - * Escaping - */ - -// nolint -const ( - ACK = 6 - TAB = 9 - LF = 10 - CR = 13 - US = 31 - SPACE = 32 - AMPERSTAND = 38 - SINGLE_QUOTE = 39 - PLUS = 43 - NINE = 57 - QUESTION = 63 - UPPERCASE_Z = 90 - OPEN_BRACKET = 91 - BACKSLASH = 92 - UNDERSCORE = 95 - CLOSE_BRACKET = 93 - BACKTICK = 96 - LOWERCASE_Z = 122 - TILDA = 126 - DEL = 127 -) - -// https://github.com/solidsnack/shell-escape/blob/master/Text/ShellEscape/Bash.hs -/* -A Bash escaped string. The strings are wrapped in @$\'...\'@ if any -bytes within them must be escaped; otherwise, they are left as is. -Newlines and other control characters are represented as ANSI escape -sequences. High bytes are represented as hex codes. Thus Bash escaped -strings will always fit on one line and never contain non-ASCII bytes. -*/ -func BashEscape(str string) string { - if str == "" { - return "''" - } - // var too short - //nolint:varnamelen - in := []byte(str) - out := "" - i := 0 - // var too short - //nolint:varnamelen - l := len(in) - escape := false - - hex := func(char byte) { - escape = true - out += fmt.Sprintf("\\x%02x", char) - } - - backslash := func(char byte) { - escape = true - out += string([]byte{BACKSLASH, char}) - } - - escaped := func(str string) { - escape = true - out += str - } - - quoted := func(char byte) { - escape = true - out += string([]byte{char}) - } - - literal := func(char byte) { - out += string([]byte{char}) - } - - for i < l { - char := in[i] - switch { - case char == ACK: - hex(char) - case char == TAB: - escaped(`\t`) - case char == LF: - escaped(`\n`) - case char == CR: - escaped(`\r`) - case char <= US: - hex(char) - case char <= AMPERSTAND: - quoted(char) - case char == SINGLE_QUOTE: - backslash(char) - case char <= PLUS: - quoted(char) - case char <= NINE: - literal(char) - case char <= QUESTION: - quoted(char) - case char <= UPPERCASE_Z: - literal(char) - case char == OPEN_BRACKET: - quoted(char) - case char == BACKSLASH: - backslash(char) - case char == UNDERSCORE: - literal(char) - case char <= CLOSE_BRACKET: - quoted(char) - case char <= BACKTICK: - quoted(char) - case char <= TILDA: - quoted(char) - case char == DEL: - hex(char) - default: - hex(char) - } - i++ - } - - if escape { - out = "$'" + out + "'" - } - - return out -} diff --git a/internal/shenv/shell_fish.go b/internal/shenv/shell_fish.go deleted file mode 100644 index c39261ce248..00000000000 --- a/internal/shenv/shell_fish.go +++ /dev/null @@ -1,110 +0,0 @@ -package shenv - -import ( - "fmt" - "strings" -) - -type fish struct{} - -// Fish adds support for the fish shell as a host -var Fish Shell = fish{} - -const fishHook = ` -function __devbox_shellenv_eval --on-event fish_prompt; - devbox shellenv --config {{ .ProjectDir }} | source; -end; -` - -func (sh fish) Hook() (string, error) { - return fishHook, nil -} - -func (sh fish) Export(e ShellExport) (out string) { - for key, value := range e { - if value == nil { - out += sh.unset(key) - } else { - out += sh.export(key, *value) - } - } - return out -} - -func (sh fish) Dump(env Env) (out string) { - for key, value := range env { - out += sh.export(key, value) - } - return out -} - -func (sh fish) export(key, value string) string { - if key == "PATH" { - command := "set -x -g PATH" - for _, path := range strings.Split(value, ":") { - command += " " + sh.escape(path) - } - return command + ";" - } - return "set -x -g " + sh.escape(key) + " " + sh.escape(value) + ";" -} - -func (sh fish) unset(key string) string { - return "set -e -g " + sh.escape(key) + ";" -} - -func (sh fish) escape(str string) string { - // var too short - //nolint:varnamelen - in := []byte(str) - out := "'" - i := 0 - // var too short - //nolint:varnamelen - l := len(in) - - hex := func(char byte) { - out += fmt.Sprintf("'\\X%02x'", char) - } - - backslash := func(char byte) { - out += string([]byte{BACKSLASH, char}) - } - - escaped := func(str string) { - out += "'" + str + "'" - } - - literal := func(char byte) { - out += string([]byte{char}) - } - - for i < l { - char := in[i] - switch { - case char == TAB: - escaped(`\t`) - case char == LF: - escaped(`\n`) - case char == CR: - escaped(`\r`) - case char <= US: - hex(char) - case char == SINGLE_QUOTE: - backslash(char) - case char == BACKSLASH: - backslash(char) - case char <= TILDA: - literal(char) - case char == DEL: - hex(char) - default: - hex(char) - } - i++ - } - - out += "'" - - return out -} diff --git a/internal/shenv/shell_ksh.go b/internal/shenv/shell_ksh.go deleted file mode 100644 index 2e246084828..00000000000 --- a/internal/shenv/shell_ksh.go +++ /dev/null @@ -1,30 +0,0 @@ -package shenv - -type ksh struct{} - -// Ksh adds support the korn shell -var Ksh Shell = ksh{} - -// um, this is ChatGPT writing it. I need to verify and test -const kshHook = ` -_devbox_hook() { - eval "$(devbox shellenv --config {{ .ProjectDir }})"; -} -if [[ "$(typeset -f precmd)" != *"_devbox_hook"* ]]; then - function precmd { - devbox_hook - } -fi -` - -func (sh ksh) Hook() (string, error) { - return kshHook, nil -} - -func (sh ksh) Export(e ShellExport) (out string) { - panic("not implemented") -} - -func (sh ksh) Dump(env Env) (out string) { - panic("not implemented") -} diff --git a/internal/shenv/shell_posix.go b/internal/shenv/shell_posix.go deleted file mode 100644 index 194eae68f35..00000000000 --- a/internal/shenv/shell_posix.go +++ /dev/null @@ -1,34 +0,0 @@ -package shenv - -type posix struct{} - -// Posix adds support for posix-compatible shells -// Specifically, in the context of devbox, this includes -// `dash`, `ash`, and `shell` -var Posix Shell = posix{} - -// um, this is ChatGPT writing it. I need to verify and test -const posixHook = ` -_devbox_hook() { - local previous_exit_status=$? - trap : INT - eval "$(devbox shellenv --config {{ .ProjectDir }})" - trap - INT - return $previous_exit_status -} -if [ -z "$PROMPT_COMMAND" ] || ! printf "%s" "$PROMPT_COMMAND" | grep -q "_devbox_hook"; then - PROMPT_COMMAND="_devbox_hook${PROMPT_COMMAND:+;$PROMPT_COMMAND}" -fi -` - -func (sh posix) Hook() (string, error) { - return posixHook, nil -} - -func (sh posix) Export(e ShellExport) (out string) { - panic("not implemented") -} - -func (sh posix) Dump(env Env) (out string) { - panic("not implemented") -} diff --git a/internal/shenv/shell_unknown.go b/internal/shenv/shell_unknown.go deleted file mode 100644 index 8a82267350e..00000000000 --- a/internal/shenv/shell_unknown.go +++ /dev/null @@ -1,24 +0,0 @@ -package shenv - -type unknown struct{} - -// UnknownSh adds support the unknown shell. This serves -// as a fallback alternative to outright failure. -var UnknownSh Shell = unknown{} - -const unknownHook = ` -echo "Warning: this shell will not update its environment. -Please exit and re-enter shell after making any changes that may affect the devbox generated environment.\n" -` - -func (sh unknown) Hook() (string, error) { - return unknownHook, nil -} - -func (sh unknown) Export(e ShellExport) (out string) { - panic("not implemented") -} - -func (sh unknown) Dump(env Env) (out string) { - panic("not implemented") -} diff --git a/internal/shenv/shell_zsh.go b/internal/shenv/shell_zsh.go deleted file mode 100644 index 936b2846a3c..00000000000 --- a/internal/shenv/shell_zsh.go +++ /dev/null @@ -1,53 +0,0 @@ -package shenv - -// ZSH is a singleton instance of ZSH_T -type zsh struct{} - -// Zsh adds support for the venerable Z shell. -var Zsh Shell = zsh{} - -const zshHook = ` -_devbox_hook() { - trap -- '' SIGINT; - eval "$(devbox shellenv --config {{ .ProjectDir }})"; - trap - SIGINT; -} -typeset -ag precmd_functions; -if [[ -z "${precmd_functions[(r)_devbox_hook]+1}" ]]; then - precmd_functions=( _devbox_hook ${precmd_functions[@]} ) -fi -` - -func (sh zsh) Hook() (string, error) { - return zshHook, nil -} - -func (sh zsh) Export(e ShellExport) (out string) { - for key, value := range e { - if value == nil { - out += sh.unset(key) - } else { - out += sh.export(key, *value) - } - } - return out -} - -func (sh zsh) Dump(env Env) (out string) { - for key, value := range env { - out += sh.export(key, value) - } - return out -} - -func (sh zsh) export(key, value string) string { - return "export " + sh.escape(key) + "=" + sh.escape(value) + ";" -} - -func (sh zsh) unset(key string) string { - return "unset " + sh.escape(key) + ";" -} - -func (sh zsh) escape(str string) string { - return BashEscape(str) -} diff --git a/internal/shenv/shenv.go b/internal/shenv/shenv.go deleted file mode 100644 index 2cabcc030f6..00000000000 --- a/internal/shenv/shenv.go +++ /dev/null @@ -1,49 +0,0 @@ -package shenv - -type Env map[string]string - -// Shell is the interface that represents the interaction with the host shell. -type Shell interface { - // Hook is the string that gets evaluated into the host shell config and - // setups direnv as a prompt hook. - Hook() (string, error) - - // Export outputs the ShellExport as an evaluatable string on the host shell - Export(e ShellExport) string - - // Dump outputs and evaluatable string that sets the env in the host shell - Dump(env Env) string -} - -// ShellExport represents environment variables to add and remove on the host -// shell. -type ShellExport map[string]*string - -// Add represents the addition of a new environment variable -func (e ShellExport) Add(key, value string) { - e[key] = &value -} - -// Remove represents the removal of a given `key` environment variable. -func (e ShellExport) Remove(key string) { - e[key] = nil -} - -// DetectShell returns a Shell instance from the given shell name -// TODO: use a single common "enum" for both shenv and DevboxShell -func DetectShell(target string) Shell { - switch target { - case "bash": - return Bash - case "fish": - return Fish - case "ksh": - return Ksh - case "posix": - return Posix - case "zsh": - return Zsh - default: - return UnknownSh - } -} diff --git a/internal/ux/stepper/stepper.go b/internal/ux/stepper/stepper.go deleted file mode 100644 index 0f93bae5ffa..00000000000 --- a/internal/ux/stepper/stepper.go +++ /dev/null @@ -1,54 +0,0 @@ -// Copyright 2024 Jetify Inc. and contributors. All rights reserved. -// Use of this source code is governed by the license in the LICENSE file. - -package stepper - -import ( - "fmt" - "io" - "time" - - "github.com/briandowns/spinner" - "github.com/fatih/color" -) - -type Stepper struct { - spinner *spinner.Spinner -} - -func Start(w io.Writer, format string, a ...any) *Stepper { - spinner := spinner.New(spinner.CharSets[11], 100*time.Millisecond, spinner.WithWriter(w)) - err := spinner.Color("magenta") - if err != nil { - panic(err) - } - spinner.Suffix = " " + fmt.Sprintf(format, a...) - spinner.Start() - return &Stepper{ - spinner: spinner, - } -} - -func (s *Stepper) Stop(format string, a ...any) { - msg := fmt.Sprintf(format, a...) - s.spinner.FinalMSG = fmt.Sprintf("%s %s\n", color.BlueString("→"), msg) - s.spinner.Stop() -} - -func (s *Stepper) Fail(format string, a ...any) { - msg := fmt.Sprintf(format, a...) - s.spinner.FinalMSG = fmt.Sprintf("%s %s\n", color.RedString("✘"), msg) - s.spinner.Stop() -} - -func (s *Stepper) Success(format string, a ...any) { - msg := fmt.Sprintf(format, a...) - s.spinner.FinalMSG = fmt.Sprintf("%s %s\n", color.GreenString("✓"), msg) - s.spinner.Stop() -} - -func (s *Stepper) Display(format string, a ...any) { - msg := fmt.Sprintf(format, a...) - // we need to add a space prefix to give a small gap between the spinner animation and the msg - s.spinner.Suffix = fmt.Sprintf(" %s", msg) -} diff --git a/vendor-hash b/vendor-hash index b87e65da4b3..b749c86f56a 100644 --- a/vendor-hash +++ b/vendor-hash @@ -1 +1 @@ -sha256-js0dxnLBSnfhgjigTmQAh7D9t6ZeSHf7k6Xd3RIBUjo= +sha256-xsx+bFjvYpLYE+Sok+4zlsK6i9QLg04fdWQoN5zC2CY=