···137137138138## Under the hood
139139140140-AppHerder reads the AppImage's filesystem to grab its icon and desktop entry. SquashFS images are parsed in-process. DwarFS images use the system dwarfsextract tool, falling back to the AppImage's own runtime when the tool isn't available. Everything it writes is tagged, so uninstall and sync only touch its own files.
140140+AppHerder reads the AppImage's filesystem to grab its icon and desktop entry. SquashFS images are parsed in-process. DwarFS images require `dwarfsextract`; the AppHerder CLI AppImage bundles it, while native and source installs need `dwarfsextract` available on `PATH`. AppHerder does not automatically fall back to `--appimage-extract`, because that executes the AppImage runtime. Everything it writes is tagged, so uninstall and sync only touch its own files.
141141142142## License
143143
+20-27
internal/appherder/dwarfs.go
···66 "io/fs"
77 "os"
88 "os/exec"
99- "path/filepath"
109 "time"
1110)
12111313-// openDwarFS extracts the DwarFS payload from an AppImage. Tries the system
1414-// dwarfsextract tool first; falls back to the AppImage's own --appimage-extract
1515-// as a last resort. Returns the extracted filesystem and a cleanup function.
1212+// openDwarFS extracts the DwarFS payload from an AppImage using dwarfsextract.
1313+// It intentionally does not fall back to --appimage-extract, because that
1414+// executes the AppImage runtime.
1615func openDwarFS(ctx context.Context, appimagePath string) (fs.FS, func(), error) {
1716 dir, err := os.MkdirTemp("", "appherder-dwarfs")
1817 if err != nil {
···2322 ctx, cancel := context.WithTimeout(ctx, 5*time.Minute)
2423 defer cancel()
25242626- cmd := extractCommand(ctx, appimagePath, dir)
2525+ cmd, err := extractCommand(ctx, appimagePath, dir)
2626+ if err != nil {
2727+ cleanup()
2828+ return nil, nil, err
2929+ }
2730 if out, err := cmd.CombinedOutput(); err != nil {
2831 cleanup()
2932 return nil, nil, fmt.Errorf("extract DwarFS AppImage: %w\n%s", err, out)
3033 }
31343232- // dwarfsextract -o extracts directly; --appimage-extract nests under squashfs-root.
3333- root := dir
3434- if _, err := os.Stat(filepath.Join(dir, "squashfs-root")); err == nil {
3535- root = filepath.Join(dir, "squashfs-root")
3636- }
3737- return os.DirFS(root), cleanup, nil
3535+ return os.DirFS(dir), cleanup, nil
3836}
39374040-func extractCommand(ctx context.Context, appimagePath, destDir string) *exec.Cmd {
3838+func extractCommand(ctx context.Context, appimagePath, destDir string) (*exec.Cmd, error) {
4139 extract, err := exec.LookPath("dwarfsextract")
4242- if err == nil {
4343- return exec.CommandContext(ctx, extract,
4444- "--input="+appimagePath,
4545- "--output="+destDir,
4646- "--pattern=**.desktop",
4747- "--pattern=**.png",
4848- "--pattern=**.svg",
4949- "--pattern=.DirIcon",
5050- )
4040+ if err != nil {
4141+ return nil, fmt.Errorf("find dwarfsextract: %w", err)
5142 }
5252- // Fall back to the AppImage's own --appimage-extract. The file must be
5353- // executable for this to work.
5454- os.Chmod(appimagePath, 0o755)
5555- cmd := exec.CommandContext(ctx, appimagePath, "--appimage-extract")
5656- cmd.Dir = destDir
5757- return cmd
4343+ return exec.CommandContext(ctx, extract,
4444+ "--input="+appimagePath,
4545+ "--output="+destDir,
4646+ "--pattern=**.desktop",
4747+ "--pattern=**.png",
4848+ "--pattern=**.svg",
4949+ "--pattern=.DirIcon",
5050+ ), nil
5851}
59526053// isDwarFS reports whether the payload at offset starts with the DwarFS magic.