diff --git a/.github/workflows/go-ci.yml b/.github/workflows/go-ci.yml index 5c0698fda..c71e61510 100644 --- a/.github/workflows/go-ci.yml +++ b/.github/workflows/go-ci.yml @@ -28,6 +28,15 @@ jobs: - name: Checkout uses: actions/checkout@v4 + - name: Install flatpak + run: sudo apt update && sudo apt install -y flatpak + + - name: Add flathub + run: sudo flatpak remote-add --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo + + - name: Add a flatpak that mutagen could support + run: sudo flatpak install -y org.freedesktop.Platform/x86_64/24.08 app.zen_browser.zen + - name: Set up Go uses: actions/setup-go@v5 with: diff --git a/core/internal/utils/flatpak.go b/core/internal/utils/flatpak.go new file mode 100644 index 000000000..ba721b5ea --- /dev/null +++ b/core/internal/utils/flatpak.go @@ -0,0 +1,78 @@ +package utils + +import ( + "bytes" + "errors" + "os/exec" + "strings" +) + +func FlatpakInPath() bool { + _, err := exec.LookPath("flatpak") + return err == nil +} + +func FlatpakExists(name string) bool { + if !FlatpakInPath() { + return false + } + + cmd := exec.Command("flatpak", "info", name) + err := cmd.Run() + return err == nil +} + +func FlatpakSearchBySubstring(substring string) bool { + if !FlatpakInPath() { + return false + } + + cmd := exec.Command("flatpak", "list", "--app") + var stdout bytes.Buffer + cmd.Stdout = &stdout + + if err := cmd.Run(); err != nil { + return false + } + + out := stdout.String() + + for line := range strings.SplitSeq(out, "\n") { + fields := strings.Fields(line) + if len(fields) > 1 { + id := fields[1] + idParts := strings.Split(id, ".") + // We are assuming that the last part of the ID is + // the package name we're looking for. This might + // not always be true, some developers use arbitrary + // suffixes. + if len(idParts) > 0 && idParts[len(idParts)-1] == substring { + cmd := exec.Command("flatpak", "info", id) + err := cmd.Run() + return err == nil + } + } + } + return false +} + +func FlatpakInstallationDir(name string) (string, error) { + if !FlatpakInPath() { + return "", errors.New("flatpak not found in PATH") + } + + cmd := exec.Command("flatpak", "info", "--show-location", name) + var stdout bytes.Buffer + cmd.Stdout = &stdout + + if err := cmd.Run(); err != nil { + return "", errors.New("flatpak not installed: " + name) + } + + location := strings.TrimSpace(stdout.String()) + if location == "" { + return "", errors.New("installation directory not found for: " + name) + } + + return location, nil +} diff --git a/core/internal/utils/flatpak_test.go b/core/internal/utils/flatpak_test.go new file mode 100644 index 000000000..9d4ee99a2 --- /dev/null +++ b/core/internal/utils/flatpak_test.go @@ -0,0 +1,210 @@ +package utils + +import ( + "os" + "path/filepath" + "strings" + "testing" +) + +func TestFlatpakInPathAvailable(t *testing.T) { + result := FlatpakInPath() + if !result { + t.Skip("flatpak not in PATH") + } + if !result { + t.Errorf("expected true when flatpak is in PATH") + } +} + +func TestFlatpakInPathUnavailable(t *testing.T) { + tempDir := t.TempDir() + t.Setenv("PATH", tempDir) + + result := FlatpakInPath() + if result { + t.Errorf("expected false when flatpak not in PATH, got true") + } +} + +func TestFlatpakExistsValidPackage(t *testing.T) { + if !FlatpakInPath() { + t.Skip("flatpak not in PATH") + } + + result := FlatpakExists("com.nonexistent.package.test") + if result { + t.Logf("package exists (unexpected but not an error)") + } +} + +func TestFlatpakExistsNoFlatpak(t *testing.T) { + tempDir := t.TempDir() + t.Setenv("PATH", tempDir) + + result := FlatpakExists("any.package.name") + if result { + t.Errorf("expected false when flatpak not in PATH, got true") + } +} + +func TestFlatpakSearchBySubstringNoFlatpak(t *testing.T) { + tempDir := t.TempDir() + t.Setenv("PATH", tempDir) + + result := FlatpakSearchBySubstring("test") + if result { + t.Errorf("expected false when flatpak not in PATH, got true") + } +} + +func TestFlatpakSearchBySubstringNonexistent(t *testing.T) { + if !FlatpakInPath() { + t.Skip("flatpak not in PATH") + } + + result := FlatpakSearchBySubstring("ThisIsAVeryUnlikelyPackageName12345") + if result { + t.Errorf("expected false for nonexistent package substring") + } +} + +func TestFlatpakInstallationDirNoFlatpak(t *testing.T) { + tempDir := t.TempDir() + t.Setenv("PATH", tempDir) + + _, err := FlatpakInstallationDir("any.package.name") + if err == nil { + t.Errorf("expected error when flatpak not in PATH") + } + if err != nil && !strings.Contains(err.Error(), "not found in PATH") { + t.Errorf("expected 'not found in PATH' error, got: %v", err) + } +} + +func TestFlatpakInstallationDirNonexistent(t *testing.T) { + if !FlatpakInPath() { + t.Skip("flatpak not in PATH") + } + + _, err := FlatpakInstallationDir("com.nonexistent.package.test") + if err == nil { + t.Errorf("expected error for nonexistent package") + } + if err != nil && !strings.Contains(err.Error(), "not installed") { + t.Errorf("expected 'not installed' error, got: %v", err) + } +} + +func TestFlatpakInstallationDirValid(t *testing.T) { + if !FlatpakInPath() { + t.Skip("flatpak not in PATH") + } + + // This test requires a known installed flatpak + // We can't guarantee any specific flatpak is installed, + // so we'll skip if we can't find a common one + commonFlatpaks := []string{ + "org.mozilla.firefox", + "org.gnome.Calculator", + "org.freedesktop.Platform", + } + + var testPackage string + for _, pkg := range commonFlatpaks { + if FlatpakExists(pkg) { + testPackage = pkg + break + } + } + + if testPackage == "" { + t.Skip("no common flatpak packages found for testing") + } + + result, err := FlatpakInstallationDir(testPackage) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if result == "" { + t.Errorf("expected non-empty installation directory") + } + if !strings.Contains(result, testPackage) { + t.Logf("installation directory %s doesn't contain package name (may be expected)", result) + } +} + +func TestFlatpakExistsCommandFailure(t *testing.T) { + if !FlatpakInPath() { + t.Skip("flatpak not in PATH") + } + + // Mock a failing flatpak command through PATH interception + tempDir := t.TempDir() + fakeFlatpak := filepath.Join(tempDir, "flatpak") + + script := "#!/bin/sh\nexit 1\n" + err := os.WriteFile(fakeFlatpak, []byte(script), 0755) + if err != nil { + t.Fatalf("failed to create fake flatpak: %v", err) + } + + originalPath := os.Getenv("PATH") + t.Setenv("PATH", tempDir+":"+originalPath) + + result := FlatpakExists("test.package") + if result { + t.Errorf("expected false when flatpak command fails, got true") + } +} + +func TestFlatpakSearchBySubstringCommandFailure(t *testing.T) { + if !FlatpakInPath() { + t.Skip("flatpak not in PATH") + } + + // Mock a failing flatpak command through PATH interception + tempDir := t.TempDir() + fakeFlatpak := filepath.Join(tempDir, "flatpak") + + script := "#!/bin/sh\nexit 1\n" + err := os.WriteFile(fakeFlatpak, []byte(script), 0755) + if err != nil { + t.Fatalf("failed to create fake flatpak: %v", err) + } + + originalPath := os.Getenv("PATH") + t.Setenv("PATH", tempDir+":"+originalPath) + + result := FlatpakSearchBySubstring("test") + if result { + t.Errorf("expected false when flatpak command fails, got true") + } +} + +func TestFlatpakInstallationDirCommandFailure(t *testing.T) { + if !FlatpakInPath() { + t.Skip("flatpak not in PATH") + } + + // Mock a failing flatpak command through PATH interception + tempDir := t.TempDir() + fakeFlatpak := filepath.Join(tempDir, "flatpak") + + script := "#!/bin/sh\nexit 1\n" + err := os.WriteFile(fakeFlatpak, []byte(script), 0755) + if err != nil { + t.Fatalf("failed to create fake flatpak: %v", err) + } + + originalPath := os.Getenv("PATH") + t.Setenv("PATH", tempDir+":"+originalPath) + + _, err = FlatpakInstallationDir("test.package") + if err == nil { + t.Errorf("expected error when flatpak command fails") + } + if err != nil && !strings.Contains(err.Error(), "not installed") { + t.Errorf("expected 'not installed' error, got: %v", err) + } +}