From 59cb805f85dfc7bdff30c0038ce7e986113d80a8 Mon Sep 17 00:00:00 2001 From: "Antoine C." Date: Sun, 19 Jan 2025 16:35:05 +0000 Subject: [PATCH] feat(VSCode): add support for IDE inside Flatpak --- cmd/up.go | 26 ++++++-- pkg/config/ide.go | 1 + pkg/ide/ideparse/parse.go | 7 ++ pkg/ide/vscode/open.go | 136 +++++++++++++++++++++++--------------- pkg/ide/vscode/vscode.go | 11 +-- 5 files changed, 116 insertions(+), 65 deletions(-) diff --git a/cmd/up.go b/cmd/up.go index 500120fa4..39bb40b3e 100644 --- a/cmd/up.go +++ b/cmd/up.go @@ -197,6 +197,7 @@ func (cmd *UpCmd) Run( } // configure container ssh + var sshConfigPath string if cmd.ConfigureSSH { devPodHome := "" envDevPodHome, ok := os.LookupEnv("DEVPOD_HOME") @@ -205,7 +206,7 @@ func (cmd *UpCmd) Run( } setupGPGAgentForwarding := cmd.GPGAgentForwarding || devPodConfig.ContextOption(config.ContextOptionGPGAgentForwarding) == "true" - err = configureSSH(devPodConfig, client, cmd.SSHConfigPath, user, workdir, setupGPGAgentForwarding, devPodHome) + sshConfigPath, err = configureSSH(devPodConfig, client, cmd.SSHConfigPath, user, workdir, setupGPGAgentForwarding, devPodHome) if err != nil { return err } @@ -238,6 +239,7 @@ func (cmd *UpCmd) Run( result.SubstitutionContext.ContainerWorkspaceFolder, vscode.Options.GetValue(ideConfig.Options, vscode.OpenNewWindow) == "true", vscode.FlavorStable, + sshConfigPath, log, ) case string(config.IDEVSCodeInsiders): @@ -247,6 +249,7 @@ func (cmd *UpCmd) Run( result.SubstitutionContext.ContainerWorkspaceFolder, vscode.Options.GetValue(ideConfig.Options, vscode.OpenNewWindow) == "true", vscode.FlavorInsiders, + sshConfigPath, log, ) case string(config.IDECursor): @@ -256,6 +259,7 @@ func (cmd *UpCmd) Run( result.SubstitutionContext.ContainerWorkspaceFolder, vscode.Options.GetValue(ideConfig.Options, vscode.OpenNewWindow) == "true", vscode.FlavorCursor, + sshConfigPath, log, ) case string(config.IDECodium): @@ -265,6 +269,17 @@ func (cmd *UpCmd) Run( result.SubstitutionContext.ContainerWorkspaceFolder, vscode.Options.GetValue(ideConfig.Options, vscode.OpenNewWindow) == "true", vscode.FlavorCodium, + sshConfigPath, + log, + ) + case string(config.IDECodiumInsiders): + return vscode.Open( + ctx, + client.Workspace(), + result.SubstitutionContext.ContainerWorkspaceFolder, + vscode.Options.GetValue(ideConfig.Options, vscode.OpenNewWindow) == "true", + vscode.FlavorCodiumInsiders, + sshConfigPath, log, ) case string(config.IDEPositron): @@ -274,6 +289,7 @@ func (cmd *UpCmd) Run( result.SubstitutionContext.ContainerWorkspaceFolder, vscode.Options.GetValue(ideConfig.Options, vscode.OpenNewWindow) == "true", vscode.FlavorPositron, + sshConfigPath, log, ) case string(config.IDEOpenVSCode): @@ -995,10 +1011,10 @@ func startBrowserTunnel( return nil } -func configureSSH(c *config.Config, client client2.BaseWorkspaceClient, sshConfigPath, user, workdir string, gpgagent bool, devPodHome string) error { +func configureSSH(c *config.Config, client client2.BaseWorkspaceClient, sshConfigPath, user, workdir string, gpgagent bool, devPodHome string) (string, error) { path, err := devssh.ResolveSSHConfigPath(sshConfigPath) if err != nil { - return errors.Wrap(err, "Invalid ssh config path") + return "", errors.Wrap(err, "Invalid ssh config path") } sshConfigPath = path @@ -1013,10 +1029,10 @@ func configureSSH(c *config.Config, client client2.BaseWorkspaceClient, sshConfi log.Default, ) if err != nil { - return err + return "", err } - return nil + return sshConfigPath, nil } func mergeDevPodUpOptions(baseOptions *provider2.CLIOptions) error { diff --git a/pkg/config/ide.go b/pkg/config/ide.go index 3282913d7..33b4f29ec 100644 --- a/pkg/config/ide.go +++ b/pkg/config/ide.go @@ -24,5 +24,6 @@ const ( IDEPositron IDE = "positron" IDEMarimo IDE = "marimo" IDECodium IDE = "codium" + IDECodiumInsiders IDE = "codium-insiders" IDEZed IDE = "zed" ) diff --git a/pkg/ide/ideparse/parse.go b/pkg/ide/ideparse/parse.go index 34d77be50..b733485be 100644 --- a/pkg/ide/ideparse/parse.go +++ b/pkg/ide/ideparse/parse.go @@ -164,6 +164,13 @@ var AllowedIDEs = []AllowedIDE{ Icon: "https://devpod.sh/assets/codium.svg", Experimental: true, }, + { + Name: config.IDECodiumInsiders, + DisplayName: "Codium Insiders", + Options: vscode.Options, + Icon: "https://devpod.sh/assets/codium_insiders.svg", // TODO to be uploaded + Experimental: true, + }, { Name: config.IDEPositron, DisplayName: "Positron", diff --git a/pkg/ide/vscode/open.go b/pkg/ide/vscode/open.go index 18aae60da..55097a6e0 100644 --- a/pkg/ide/vscode/open.go +++ b/pkg/ide/vscode/open.go @@ -6,6 +6,7 @@ import ( "fmt" "os/exec" "runtime" + "slices" "strings" "github.com/loft-sh/devpod/pkg/command" @@ -13,10 +14,19 @@ import ( "github.com/skratchdot/open-golang/open" ) -func Open(ctx context.Context, workspace, folder string, newWindow bool, flavor Flavor, log log.Logger) error { +const ( + FlatpakStable string = "com.visualstudio.code" + FlatpakInsiders string = "com.visualstudio.code.insiders" + FlatpakCodium string = "com.vscodium.codium" + FlatpakCodiumInsiders string = "com.vscodium.codium-insiders" +) + +func Open(ctx context.Context, workspace, folder string, newWindow bool, flavor Flavor, sshConfigPath string, log log.Logger) error { log.Infof("Starting %s...", flavor.DisplayName()) - cliErr := openViaCLI(ctx, workspace, folder, newWindow, flavor, log) - if cliErr == nil { + cliErr := openViaCLI(ctx, workspace, folder, newWindow, flavor, sshConfigPath, log) + if cliErr != nil { + log.Debugf("Error opening %s via cli: %v", flavor, cliErr) + } else { return nil } @@ -41,6 +51,8 @@ func openViaBrowser(workspace, folder string, newWindow bool, flavor Flavor, log protocol = `positron://` case FlavorCodium: protocol = `codium://` + case FlavorCodiumInsiders: + protocol = `codium-insiders://` } openURL := protocol + `vscode-remote/ssh-remote+` + workspace + `.devpod/` + folder @@ -58,20 +70,44 @@ func openViaBrowser(workspace, folder string, newWindow bool, flavor Flavor, log return nil } -func openViaCLI(ctx context.Context, workspace, folder string, newWindow bool, flavor Flavor, log log.Logger) error { +func openViaCLI(ctx context.Context, workspace, folder string, newWindow bool, flavor Flavor, sshConfigPath string, log log.Logger) error { // try to find code cli - codePath := findCLI(flavor) - if codePath == "" { + codePath := findCLI(flavor, log) + if codePath == nil { return fmt.Errorf("couldn't find the %s binary", flavor) } + if codePath[0] == "flatpak" { + log.Debugf("Running with Flatpak suing the package %s.", codePath[2]) + out, err := exec.Command(codePath[0], "ps", "--columns=application").Output() + if err != nil { + return command.WrapCommandError(out, err) + } + splitted := strings.Split(string(out), "\n") + foundRunning := false + // Ignore the header + for _, str := range splitted[1:] { + if strings.TrimSpace(str) == codePath[2] { + foundRunning = true + break + } + } + + if foundRunning { + log.Warnf("The IDE is already running via Flatpak. If you are encountering SSH connectivity issues, make sure to give read access to your SSH config file (e.g flatpak override %s --filesystem=%s) and restart your IDE.", codePath[2], sshConfigPath) + } + + codePath = slices.Insert(codePath, 2, fmt.Sprintf("--filesystem=%s:ro", sshConfigPath)) + } + sshExtension := "ms-vscode-remote.remote-ssh" - if flavor == FlavorCodium { + if flavor == FlavorCodium || flavor == FlavorCodiumInsiders { sshExtension = "jeanp413.open-remote-ssh" } // make sure ms-vscode-remote.remote-ssh is installed - out, err := exec.Command(codePath, "--list-extensions").Output() + listArgs := append(codePath, "--list-extensions") + out, err := exec.Command(listArgs[0], listArgs[1:]...).Output() if err != nil { return command.WrapCommandError(out, err) } @@ -88,9 +124,9 @@ func openViaCLI(ctx context.Context, workspace, folder string, newWindow bool, f // install remote-ssh extension if !found { - args := []string{"--install-extension", sshExtension} - log.Debugf("Run vscode command %s %s", codePath, strings.Join(args, " ")) - out, err := exec.CommandContext(ctx, codePath, args...).Output() + args := append(codePath, "--install-extension", sshExtension) + log.Debugf("Run vscode command %s %s", args[0], strings.Join(args[1:], " ")) + out, err := exec.CommandContext(ctx, args[0], args[1:]...).Output() if err != nil { return fmt.Errorf("install ssh extension: %w", command.WrapCommandError(out, err)) } @@ -108,9 +144,10 @@ func openViaCLI(ctx context.Context, workspace, folder string, newWindow bool, f } // Needs to be separated by `=` because of windows folderUriArg := fmt.Sprintf("--folder-uri=vscode-remote://ssh-remote+%s.devpod/%s", workspace, folder) + args = append(codePath, args...) args = append(args, folderUriArg) - log.Debugf("Run %s command %s %s", flavor.DisplayName(), codePath, strings.Join(args, " ")) - out, err = exec.CommandContext(ctx, codePath, args...).CombinedOutput() + log.Debugf("Run %s command %s %s", flavor.DisplayName(), args[0], strings.Join(args[1:], " ")) + out, err = exec.CommandContext(ctx, args[0], args[1:]...).CombinedOutput() if err != nil { return command.WrapCommandError(out, err) } @@ -118,56 +155,45 @@ func openViaCLI(ctx context.Context, workspace, folder string, newWindow bool, f return nil } -func findCLI(flavor Flavor) string { - if flavor == FlavorStable { - if command.Exists("code") { - return "code" - } else if runtime.GOOS == "darwin" && command.Exists("/Applications/Visual Studio Code.app/Contents/Resources/app/bin/code") { - return "/Applications/Visual Studio Code.app/Contents/Resources/app/bin/code" - } - - return "" +func existsInFlatpak(packageName string, log log.Logger) bool { + if err := exec.Command("flatpak", "info", packageName).Run(); err == nil { + return true + } else { + log.Debugf("Flatpak command for %s returned: %s", packageName, err) } + return false +} - if flavor == FlavorInsiders { - if command.Exists("code-insiders") { - return "code-insiders" - } else if runtime.GOOS == "darwin" && command.Exists("/Applications/Visual Studio Code - Insiders.app/Contents/Resources/app/bin/code") { - return "/Applications/Visual Studio Code - Insiders.app/Contents/Resources/app/bin/code" - } - - return "" +func getCommandArgs(execName, macOSPath, flatpakPackage string, log log.Logger) []string { + if command.Exists(execName) { + return []string{execName} } - if flavor == FlavorCursor { - if command.Exists("cursor") { - return "cursor" - } else if runtime.GOOS == "darwin" && command.Exists("/Applications/Cursor.app/Contents/Resources/app/bin/cursor") { - return "/Applications/Cursor.app/Contents/Resources/app/bin/cursor" - } - - return "" + if runtime.GOOS == "darwin" && command.Exists(macOSPath) { + return []string{macOSPath} } - if flavor == FlavorPositron { - if command.Exists("positron") { - return "positron" - } else if runtime.GOOS == "darwin" && command.Exists("/Applications/Positron.app/Contents/Resources/app/bin/positron") { - return "/Applications/Positron.app/Contents/Resources/app/bin/positron" - } - - return "" + if runtime.GOOS == "linux" && flatpakPackage != "" && command.Exists("flatpak") && existsInFlatpak(flatpakPackage, log) { + return []string{"flatpak", "run", flatpakPackage} } - if flavor == FlavorCodium { - if command.Exists("codium") { - return "codium" - } else if runtime.GOOS == "darwin" && command.Exists("/Applications/Codium.app/Contents/Resources/app/bin/codium") { - return "/Applications/Codium.app/Contents/Resources/app/bin/codium" - } + return nil +} - return "" +func findCLI(flavor Flavor, log log.Logger) []string { + switch flavor { + case FlavorStable: + return getCommandArgs("code", "/Applications/Visual Studio Code.app/Contents/Resources/app/bin/code", FlatpakStable, log) + case FlavorInsiders: + return getCommandArgs("code-insiders", "/Applications/Visual Studio Code - Insiders.app/Contents/Resources/app/bin/code", FlatpakInsiders, log) + case FlavorCursor: + return getCommandArgs("cursor", "/Applications/Cursor.app/Contents/Resources/app/bin/cursor", "", log) + case FlavorPositron: + return getCommandArgs("positron", "/Applications/Positron.app/Contents/Resources/app/bin/positron", "", log) + case FlavorCodium: + return getCommandArgs("codium", "/Applications/Codium.app/Contents/Resources/app/bin/codium", FlatpakCodium, log) + case FlavorCodiumInsiders: + return getCommandArgs("codium-insiders", "/Applications/CodiumInsiders.app/Contents/Resources/app/bin/codium-insiders", FlatpakCodiumInsiders, log) } - - return "" + return nil } diff --git a/pkg/ide/vscode/vscode.go b/pkg/ide/vscode/vscode.go index 7d004a919..50c09a386 100644 --- a/pkg/ide/vscode/vscode.go +++ b/pkg/ide/vscode/vscode.go @@ -26,11 +26,12 @@ const ( type Flavor string const ( - FlavorStable Flavor = "stable" - FlavorInsiders Flavor = "insiders" - FlavorCursor Flavor = "cursor" - FlavorPositron Flavor = "positron" - FlavorCodium Flavor = "codium" + FlavorStable Flavor = "stable" + FlavorInsiders Flavor = "insiders" + FlavorCursor Flavor = "cursor" + FlavorPositron Flavor = "positron" + FlavorCodium Flavor = "codium" + FlavorCodiumInsiders Flavor = "codium-insiders" ) func (f Flavor) DisplayName() string {