A shepherd for your Appimages.
0

Configure Feed

Select the types of activity you want to include in your feed.

add safe in-place file extractions with CalebQ42/squashfs

Aly Raffauf (Jun 17, 2026, 4:28 PM EDT) 9a0b3e57 7665ea56

+291 -81
+1
.gitignore
··· 1 1 squashfs-root/ 2 2 *.appimage 3 3 /appherder 4 + /result
+1 -28
cmd/appherder/app.go
··· 1 1 package main 2 2 3 - import ( 4 - "context" 5 - "fmt" 6 - "os" 7 - "os/exec" 8 - "strings" 9 - "time" 10 - ) 11 - 12 - const extractTimeout = 2 * time.Minute 3 + import "os" 13 4 14 5 type app struct { 15 6 homeDir func() (string, error) 16 - run func(name string, args []string, dir string) error 17 7 } 18 8 19 9 func newApp() app { 20 10 return app{ 21 11 homeDir: os.UserHomeDir, 22 - run: runCommand, 23 12 } 24 13 } 25 - 26 - func runCommand(name string, args []string, dir string) error { 27 - ctx, cancel := context.WithTimeout(context.Background(), extractTimeout) 28 - defer cancel() 29 - 30 - cmd := exec.CommandContext(ctx, name, args...) 31 - cmd.Dir = dir 32 - output, err := cmd.CombinedOutput() 33 - if ctx.Err() == context.DeadlineExceeded { 34 - return fmt.Errorf("timed out after %s", extractTimeout) 35 - } 36 - if err != nil && len(output) > 0 { 37 - return fmt.Errorf("%w: %s", err, strings.TrimSpace(string(output))) 38 - } 39 - return err 40 - }
+83 -8
cmd/appherder/appimage.go
··· 1 1 package main 2 2 3 3 import ( 4 + "encoding/binary" 5 + "errors" 4 6 "fmt" 5 7 "io" 8 + "io/fs" 6 9 "os" 7 10 "path/filepath" 11 + 12 + "github.com/CalebQ42/squashfs" 8 13 ) 9 14 10 - func (a app) extractAppImage(file string, dest string) (string, error) { 11 - file, err := filepath.Abs(file) 15 + // openAppImage exposes a type-2 AppImage's squashfs payload as an fs.FS without 16 + // executing it. The caller must invoke the returned closer to release the file. 17 + func openAppImage(file string) (fs.FS, func(), error) { 18 + f, err := os.Open(file) 19 + if err != nil { 20 + return nil, nil, fmt.Errorf("open AppImage %s: %w", file, err) 21 + } 22 + 23 + offset, err := appImageSquashfsOffset(f) 24 + if err != nil { 25 + f.Close() 26 + return nil, nil, fmt.Errorf("read AppImage %s: %w", file, err) 27 + } 28 + 29 + reader, err := squashfs.NewReaderAtOffset(f, offset) 12 30 if err != nil { 13 - return "", fmt.Errorf("resolve AppImage path %q: %w", file, err) 31 + f.Close() 32 + return nil, nil, fmt.Errorf("read AppImage filesystem %s: %w", file, err) 33 + } 34 + 35 + return squashFS{root: &reader.FS}, func() { f.Close() }, nil 36 + } 37 + 38 + // squashFS adapts a squashfs reader to fs.FS, resolving symlinks on Open since 39 + // the underlying reader returns them unfollowed. AppImages routinely symlink 40 + // the root .desktop and .DirIcon into usr/share. 41 + type squashFS struct { 42 + root *squashfs.FS 43 + } 44 + 45 + func (s squashFS) Open(name string) (fs.File, error) { 46 + file, err := s.root.OpenFile(name) 47 + if err != nil { 48 + return nil, err 49 + } 50 + for depth := 0; file.IsSymlink(); depth++ { 51 + if depth >= 40 { 52 + file.Close() 53 + return nil, fmt.Errorf("%s: too many levels of symbolic links", name) 54 + } 55 + next, ok := file.GetSymlinkFile().(*squashfs.File) 56 + file.Close() 57 + if !ok { 58 + return nil, fmt.Errorf("%s: broken symlink", name) 59 + } 60 + file = next 61 + } 62 + return file, nil 63 + } 64 + 65 + func (s squashFS) ReadDir(name string) ([]fs.DirEntry, error) { return s.root.ReadDir(name) } 66 + func (s squashFS) Glob(pattern string) ([]string, error) { return s.root.Glob(pattern) } 67 + func (s squashFS) Stat(name string) (fs.FileInfo, error) { return s.root.Stat(name) } 68 + 69 + // appImageSquashfsOffset returns the byte offset of the squashfs image appended 70 + // to the AppImage's ELF runtime, computed from the ELF section header table. 71 + func appImageSquashfsOffset(r io.ReaderAt) (int64, error) { 72 + var h [64]byte 73 + if _, err := r.ReadAt(h[:], 0); err != nil { 74 + return 0, fmt.Errorf("read ELF header: %w", err) 75 + } 76 + if h[0] != 0x7f || h[1] != 'E' || h[2] != 'L' || h[3] != 'F' { 77 + return 0, errors.New("not an AppImage (missing ELF header)") 14 78 } 15 - if err := os.Chmod(file, 0o755); err != nil { 16 - return "", fmt.Errorf("make AppImage executable %s: %w", file, err) 79 + // AppImages record their type in the otherwise-unused ELF padding bytes. 80 + if h[8] == 'A' && h[9] == 'I' && h[10] == 1 { 81 + return 0, errors.New("type-1 AppImages are not supported") 17 82 } 18 - if err := a.run(file, []string{"--appimage-extract"}, dest); err != nil { 19 - return "", fmt.Errorf("extract AppImage %s into %s: %w", file, dest, err) 83 + 84 + bo := binary.ByteOrder(binary.LittleEndian) 85 + if h[5] == 2 { 86 + bo = binary.BigEndian 20 87 } 21 - return filepath.Join(dest, "squashfs-root"), nil 88 + // The squashfs starts where the ELF ends: e_shoff + e_shnum*e_shentsize. 89 + switch h[4] { 90 + case 1: // 32-bit ELF: e_shoff@32, e_shentsize@46, e_shnum@48 91 + return int64(bo.Uint32(h[32:36])) + int64(bo.Uint16(h[46:48]))*int64(bo.Uint16(h[48:50])), nil 92 + case 2: // 64-bit ELF: e_shoff@40, e_shentsize@58, e_shnum@60 93 + return int64(bo.Uint64(h[40:48])) + int64(bo.Uint16(h[58:60]))*int64(bo.Uint16(h[60:62])), nil 94 + default: 95 + return 0, errors.New("unknown ELF class") 96 + } 22 97 } 23 98 24 99 func (a app) installAppImage(file string, appName string) (string, error) {
+52
cmd/appherder/appimage_test.go
··· 1 + package main 2 + 3 + import ( 4 + "bytes" 5 + "encoding/binary" 6 + "testing" 7 + ) 8 + 9 + // elfHeader builds a minimal little-endian ELF header with the given class 10 + // (1=32-bit, 2=64-bit) and section-header-table fields. 11 + func elfHeader(class byte, shoff uint64, shentsize, shnum uint16) []byte { 12 + h := make([]byte, 64) 13 + copy(h, []byte{0x7f, 'E', 'L', 'F'}) 14 + h[4] = class 15 + h[5] = 1 // little-endian 16 + if class == 2 { 17 + binary.LittleEndian.PutUint64(h[40:48], shoff) 18 + binary.LittleEndian.PutUint16(h[58:60], shentsize) 19 + binary.LittleEndian.PutUint16(h[60:62], shnum) 20 + } else { 21 + binary.LittleEndian.PutUint32(h[32:36], uint32(shoff)) 22 + binary.LittleEndian.PutUint16(h[46:48], shentsize) 23 + binary.LittleEndian.PutUint16(h[48:50], shnum) 24 + } 25 + return h 26 + } 27 + 28 + func TestAppImageSquashfsOffset(t *testing.T) { 29 + for _, class := range []byte{1, 2} { 30 + got, err := appImageSquashfsOffset(bytes.NewReader(elfHeader(class, 1000, 64, 10))) 31 + if err != nil { 32 + t.Fatalf("class %d: %v", class, err) 33 + } 34 + if want := int64(1000 + 64*10); got != want { 35 + t.Fatalf("class %d: offset = %d, want %d", class, got, want) 36 + } 37 + } 38 + } 39 + 40 + func TestAppImageSquashfsOffsetRejectsNonELF(t *testing.T) { 41 + if _, err := appImageSquashfsOffset(bytes.NewReader(make([]byte, 64))); err == nil { 42 + t.Fatal("expected error for non-ELF input") 43 + } 44 + } 45 + 46 + func TestAppImageSquashfsOffsetRejectsType1(t *testing.T) { 47 + h := elfHeader(2, 1000, 64, 10) 48 + h[8], h[9], h[10] = 'A', 'I', 1 49 + if _, err := appImageSquashfsOffset(bytes.NewReader(h)); err == nil { 50 + t.Fatal("expected error for type-1 AppImage") 51 + } 52 + }
+13 -6
cmd/appherder/desktop.go
··· 3 3 import ( 4 4 "fmt" 5 5 "io" 6 + "io/fs" 6 7 "os" 7 8 "path/filepath" 8 9 "sort" ··· 41 42 trailingNewline bool 42 43 } 43 44 44 - func findDesktopFile(extracted string) (*desktopFile, error) { 45 - candidates, err := filepath.Glob(filepath.Join(extracted, "*.desktop")) 45 + // findDesktopFile returns the AppImage's desktop entry, skipping the AppImage 46 + // runtime's default.desktop. The "*.desktop" glob only matches the root, so 47 + // candidates are bare filenames. 48 + func findDesktopFile(fsys fs.FS) (*desktopFile, error) { 49 + candidates, err := fs.Glob(fsys, "*.desktop") 46 50 if err != nil { 47 51 return nil, err 48 52 } 49 53 sort.Strings(candidates) 50 54 51 55 for _, candidate := range candidates { 52 - if filepath.Base(candidate) == "default.desktop" { 56 + if candidate == "default.desktop" { 53 57 continue 54 58 } 55 - desktop, err := readDesktopFile(candidate) 59 + data, err := fs.ReadFile(fsys, candidate) 56 60 if err != nil { 57 61 return nil, err 58 62 } 59 - return desktop, nil 63 + return parseDesktopFile(data), nil 60 64 } 61 65 62 66 return nil, nil ··· 67 71 if err != nil { 68 72 return nil, err 69 73 } 74 + return parseDesktopFile(data), nil 75 + } 70 76 77 + func parseDesktopFile(data []byte) *desktopFile { 71 78 text := string(data) 72 79 body := strings.TrimSuffix(text, "\n") 73 80 lines := []string{} ··· 108 115 }) 109 116 } 110 117 111 - return desktop, nil 118 + return desktop 112 119 } 113 120 114 121 func (d *desktopFile) section(name string) *desktopSection {
+29
cmd/appherder/desktop_test.go
··· 4 4 "os" 5 5 "path/filepath" 6 6 "testing" 7 + "testing/fstest" 7 8 ) 8 9 9 10 const sampleDesktopFile = `# leading comment ··· 79 80 assertDesktopExec(t, patched, "Desktop Action new-window", []string{"env", "DESKTOPINTEGRATION=1", "/home/test/AppImages/example.appimage", "--new-window", "%U"}) 80 81 assertDesktopExec(t, patched, "Desktop Action new-private-window", []string{"env", "DESKTOPINTEGRATION=1", "/home/test/AppImages/example.appimage", "--private-window", "%U"}) 81 82 assertDesktopValue(t, patched, "Desktop Action new-private-window", "Name", "New Private Window") 83 + } 84 + 85 + func TestFindDesktopFileSkipsDefault(t *testing.T) { 86 + fsys := fstest.MapFS{ 87 + "default.desktop": {Data: []byte("[Desktop Entry]\nName=Default\n")}, 88 + "app.desktop": {Data: []byte("[Desktop Entry]\nName=App\n")}, 89 + } 90 + desktop, err := findDesktopFile(fsys) 91 + if err != nil { 92 + t.Fatal(err) 93 + } 94 + if desktop == nil { 95 + t.Fatal("expected a desktop file") 96 + } 97 + assertDesktopValue(t, desktop, desktopEntrySection, "Name", "App") 98 + } 99 + 100 + func TestFindDesktopFileReturnsNilWhenOnlyDefault(t *testing.T) { 101 + fsys := fstest.MapFS{ 102 + "default.desktop": {Data: []byte("[Desktop Entry]\nName=Default\n")}, 103 + } 104 + desktop, err := findDesktopFile(fsys) 105 + if err != nil { 106 + t.Fatal(err) 107 + } 108 + if desktop != nil { 109 + t.Fatal("expected nil desktop file") 110 + } 82 111 } 83 112 84 113 func assertDesktopValue(t *testing.T, desktop *desktopFile, section string, key string, want string) {
+10 -2
cmd/appherder/files.go
··· 2 2 3 3 import ( 4 4 "io" 5 + "io/fs" 5 6 "os" 6 7 "path/filepath" 7 8 ) ··· 33 34 return os.Rename(tmpName, path) 34 35 } 35 36 36 - func copyFile(src string, dest string) error { 37 + func copyFromFS(fsys fs.FS, name string, dest string) error { 38 + in, err := fsys.Open(name) 39 + if err != nil { 40 + return err 41 + } 42 + defer in.Close() 43 + 37 44 return writeAtomic(dest, 0o644, func(w io.Writer) error { 38 - return copyTo(src, w) 45 + _, err := io.Copy(w, in) 46 + return err 39 47 }) 40 48 }
+20 -22
cmd/appherder/icons.go
··· 4 4 "fmt" 5 5 "io/fs" 6 6 "os" 7 + "path" 7 8 "path/filepath" 8 9 "strings" 9 10 ) 10 11 11 12 // resolveIcon prefers .DirIcon, then any icon at the AppImage root, then icons 12 - // in the standard theme directories. It returns "" when none is found. 13 - func resolveIcon(extracted string) string { 14 - dirIcon := filepath.Join(extracted, ".DirIcon") 15 - if info, err := os.Stat(dirIcon); err == nil && !info.IsDir() { 16 - return dirIcon 13 + // in the standard theme directories. It returns the in-archive path, or "" when 14 + // none is found. 15 + func resolveIcon(fsys fs.FS) string { 16 + if info, err := fs.Stat(fsys, ".DirIcon"); err == nil && !info.IsDir() { 17 + return ".DirIcon" 17 18 } 18 - return findFallbackIcon(extracted) 19 + return findFallbackIcon(fsys) 19 20 } 20 21 21 - func findFallbackIcon(extracted string) string { 22 + func findFallbackIcon(fsys fs.FS) string { 22 23 best := "" 23 24 bestRank := -1 24 25 var bestSize int64 25 - consider := func(path string, entry fs.DirEntry) { 26 - rank := iconRank(path) 26 + consider := func(name string, entry fs.DirEntry) { 27 + rank := iconRank(name) 27 28 if rank < 0 { 28 29 return 29 30 } ··· 33 34 } 34 35 // Prefer the higher-ranked extension, then the larger (higher-res) file. 35 36 if size := info.Size(); rank > bestRank || (rank == bestRank && size > bestSize) { 36 - best, bestRank, bestSize = path, rank, size 37 + best, bestRank, bestSize = name, rank, size 37 38 } 38 39 } 39 40 40 - if entries, err := os.ReadDir(extracted); err == nil { 41 + if entries, err := fs.ReadDir(fsys, "."); err == nil { 41 42 for _, entry := range entries { 42 43 if !entry.IsDir() { 43 - consider(filepath.Join(extracted, entry.Name()), entry) 44 + consider(entry.Name(), entry) 44 45 } 45 46 } 46 47 } 47 48 48 - for _, dir := range []string{ 49 - filepath.Join(extracted, "usr", "share", "icons"), 50 - filepath.Join(extracted, "usr", "share", "pixmaps"), 51 - } { 52 - _ = filepath.WalkDir(dir, func(path string, entry fs.DirEntry, err error) error { 49 + for _, dir := range []string{"usr/share/icons", "usr/share/pixmaps"} { 50 + _ = fs.WalkDir(fsys, dir, func(name string, entry fs.DirEntry, err error) error { 53 51 if err != nil || entry.IsDir() { 54 52 return nil 55 53 } 56 - consider(path, entry) 54 + consider(name, entry) 57 55 return nil 58 56 }) 59 57 } ··· 62 60 } 63 61 64 62 // iconRank ranks icon extensions (higher preferred); -1 means not an icon. 65 - func iconRank(path string) int { 66 - switch strings.ToLower(filepath.Ext(path)) { 63 + func iconRank(name string) int { 64 + switch strings.ToLower(path.Ext(name)) { 67 65 case ".svg": 68 66 return 2 69 67 case ".png": ··· 75 73 } 76 74 } 77 75 78 - func (a app) installIcon(icon string, appName string) (string, error) { 76 + func (a app) installIcon(fsys fs.FS, icon string, appName string) (string, error) { 79 77 home, err := a.homeDir() 80 78 if err != nil { 81 79 return "", fmt.Errorf("resolve home directory: %w", err) ··· 87 85 } 88 86 89 87 dest := filepath.Join(iconsDir, appName) 90 - if err := copyFile(icon, dest); err != nil { 88 + if err := copyFromFS(fsys, icon, dest); err != nil { 91 89 return "", fmt.Errorf("install icon %s to %s: %w", icon, dest, err) 92 90 } 93 91 return dest, nil
+47
cmd/appherder/icons_test.go
··· 1 + package main 2 + 3 + import ( 4 + "testing" 5 + "testing/fstest" 6 + ) 7 + 8 + func TestResolveIconPrefersDirIcon(t *testing.T) { 9 + fsys := fstest.MapFS{ 10 + ".DirIcon": {Data: []byte("dir")}, 11 + "app.png": {Data: []byte("png")}, 12 + "usr/share/icons/a.svg": {Data: []byte("svg")}, 13 + } 14 + if got := resolveIcon(fsys); got != ".DirIcon" { 15 + t.Fatalf("resolveIcon = %q, want .DirIcon", got) 16 + } 17 + } 18 + 19 + func TestResolveIconFallsBackToHighestRankedExtension(t *testing.T) { 20 + fsys := fstest.MapFS{ 21 + "app.png": {Data: []byte("0123456789")}, 22 + "usr/share/icons/a.svg": {Data: []byte("svg")}, 23 + } 24 + if got := resolveIcon(fsys); got != "usr/share/icons/a.svg" { 25 + t.Fatalf("resolveIcon = %q, want usr/share/icons/a.svg", got) 26 + } 27 + } 28 + 29 + func TestResolveIconPrefersLargerImageOfSameType(t *testing.T) { 30 + fsys := fstest.MapFS{ 31 + "small.png": {Data: []byte("a")}, 32 + "big.png": {Data: []byte("aaaaaaaaaa")}, 33 + } 34 + if got := resolveIcon(fsys); got != "big.png" { 35 + t.Fatalf("resolveIcon = %q, want big.png", got) 36 + } 37 + } 38 + 39 + func TestResolveIconReturnsEmptyWhenNoIcon(t *testing.T) { 40 + fsys := fstest.MapFS{ 41 + "AppRun": {Data: []byte("x")}, 42 + "app.desktop": {Data: []byte("x")}, 43 + } 44 + if got := resolveIcon(fsys); got != "" { 45 + t.Fatalf("resolveIcon = %q, want empty", got) 46 + } 47 + }
+15 -13
cmd/appherder/install.go
··· 7 7 "strings" 8 8 ) 9 9 10 - func (a app) install(appimage string) error { 11 - appimage, err := filepath.Abs(appimage) 10 + func (a app) install(appimage string) (err error) { 11 + appimage, err = filepath.Abs(appimage) 12 12 if err != nil { 13 13 return fmt.Errorf("resolve AppImage path %q: %w", appimage, err) 14 14 } 15 15 16 - tmp, err := os.MkdirTemp("", "appherder-") 17 - if err != nil { 18 - return fmt.Errorf("create extraction directory: %w", err) 19 - } 20 - defer os.RemoveAll(tmp) 21 - 22 - extracted, err := a.extractAppImage(appimage, tmp) 16 + fsys, closeAppImage, err := openAppImage(appimage) 23 17 if err != nil { 24 18 return err 25 19 } 20 + defer closeAppImage() 26 21 27 - desktop, err := findDesktopFile(extracted) 22 + // The squashfs reader parses untrusted input; turn any panic into an error. 23 + defer func() { 24 + if r := recover(); r != nil { 25 + err = fmt.Errorf("read AppImage %s: %v", appimage, r) 26 + } 27 + }() 28 + 29 + desktop, err := findDesktopFile(fsys) 28 30 if err != nil { 29 - return fmt.Errorf("find desktop file in %s: %w", extracted, err) 31 + return fmt.Errorf("find desktop file: %w", err) 30 32 } 31 33 32 - icon := resolveIcon(extracted) 34 + icon := resolveIcon(fsys) 33 35 appName := appNameFromPath(appimage) 34 36 35 37 // Patch in memory before any filesystem writes so a failure here installs nothing. ··· 48 50 } 49 51 50 52 if icon != "" { 51 - dest, err := a.installIcon(icon, appName) 53 + dest, err := a.installIcon(fsys, icon, appName) 52 54 if err != nil { 53 55 rollback() 54 56 return err
+7 -1
go.mod
··· 1 1 module github.com/alyraffauf/appherder 2 2 3 - go 1.24 3 + go 1.24.0 4 4 5 5 require ( 6 + github.com/CalebQ42/squashfs v1.4.1 6 7 github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 7 8 github.com/spf13/cobra v1.10.2 8 9 ) 9 10 10 11 require ( 11 12 github.com/inconshreveable/mousetrap v1.1.0 // indirect 13 + github.com/klauspost/compress v1.18.0 // indirect 14 + github.com/mikelolasagasti/xz v1.0.1 // indirect 15 + github.com/pierrec/lz4/v4 v4.1.22 // indirect 16 + github.com/rasky/go-lzo v0.0.0-20200203143853-96a758eda86e // indirect 12 17 github.com/spf13/pflag v1.0.9 // indirect 18 + github.com/ulikunitz/xz v0.5.12 // indirect 13 19 )
+12
go.sum
··· 1 + github.com/CalebQ42/squashfs v1.4.1 h1:tBcFMQSRQvWcY50e9r9cv2uVzNf06fcUhly0LeZg8bI= 2 + github.com/CalebQ42/squashfs v1.4.1/go.mod h1:/As5wg6ScFFaab9SaNFNHyCOsd73Q5IFPOFJCVnwWzQ= 1 3 github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= 2 4 github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= 3 5 github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= 4 6 github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs= 5 7 github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= 8 + github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= 9 + github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= 10 + github.com/mikelolasagasti/xz v1.0.1 h1:Q2F2jX0RYJUG3+WsM+FJknv+6eVjsjXNDV0KJXZzkD0= 11 + github.com/mikelolasagasti/xz v1.0.1/go.mod h1:muAirjiOUxPRXwm9HdDtB3uoRPrGnL85XHtokL9Hcgc= 12 + github.com/pierrec/lz4/v4 v4.1.22 h1:cKFw6uJDK+/gfw5BcDL0JL5aBsAFdsIT18eRtLj7VIU= 13 + github.com/pierrec/lz4/v4 v4.1.22/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= 14 + github.com/rasky/go-lzo v0.0.0-20200203143853-96a758eda86e h1:dCWirM5F3wMY+cmRda/B1BiPsFtmzXqV9b0hLWtVBMs= 15 + github.com/rasky/go-lzo v0.0.0-20200203143853-96a758eda86e/go.mod h1:9leZcVcItj6m9/CfHY5Em/iBrCz7js8LcRQGTKEEv2M= 6 16 github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 7 17 github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= 8 18 github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4= 9 19 github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY= 10 20 github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 21 + github.com/ulikunitz/xz v0.5.12 h1:37Nm15o69RwBkXM0J6A5OlE67RZTfzUxTj8fB3dfcsc= 22 + github.com/ulikunitz/xz v0.5.12/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= 11 23 go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= 12 24 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+1 -1
package.nix
··· 6 6 pname = "appherder"; 7 7 version = "dev"; 8 8 src = ./.; 9 - vendorHash = "sha256-Q+emMKLlnoRlYIe2nNZ6NKkg6bao1xj8CARkv5uiZRs="; 9 + vendorHash = "sha256-cnxgWLpc8l/dvJHgj1PkJrSYmsH88pCTISM+pf7Ulg4="; 10 10 subPackages = ["cmd/appherder"]; 11 11 }