Label jenis aplikasi dengan auto-detect (Go/AppImage/Electron/Script/Windows/Binary/PWA)
- internal/shortcut/kind.go: deteksi dari isi berkas - build info Go, magic AppImage, resources/*.asar untuk Electron, shebang untuk script, MZ untuk .exe - .exe Windows kini boleh dipasang dan dijalankan lewat wine (Exec=wine <path>); kalau wine tidak terpasang, install ditolak dengan pesan jelas - metadata menyimpan kind; chip jenis ditampilkan di setiap baris shortcut (data lama tetap tampil sebagai Executable/PWA) - dialog menampilkan hasil deteksi setelah berkas dipilih, mis. 'winbox.exe - windows' - README: bagian label otomatis + screenshot baru
This commit is contained in:
1 parent
41ada9a506
commit
d6f6c69c5a
13 files changed
+249
-13
No files matched your search
@@ -5,6 +5,7 @@ import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -65,9 +66,20 @@ func InstallExecutable(o ExecOptions) (store.Shortcut, error) {
|
||||
return sc, err
|
||||
}
|
||||
|
||||
// jenis aplikasi dideteksi otomatis; .exe Windows dijalankan lewat wine
|
||||
kind := DetectKind(o.BinaryPath)
|
||||
execArgs := []string{binDst}
|
||||
if kind == KindWindows {
|
||||
wine, err := exec.LookPath("wine")
|
||||
if err != nil {
|
||||
return sc, errors.New("this is a Windows .exe; install wine first so the shortcut can run it")
|
||||
}
|
||||
execArgs = []string{wine, binDst}
|
||||
}
|
||||
|
||||
entry := Entry{
|
||||
Name: name,
|
||||
ExecArgs: []string{binDst},
|
||||
ExecArgs: execArgs,
|
||||
Icon: iconPath,
|
||||
Categories: category(o.Category, "Utility;"),
|
||||
NoDisplay: !o.Menu,
|
||||
@@ -80,6 +92,7 @@ func InstallExecutable(o ExecOptions) (store.Shortcut, error) {
|
||||
Slug: sl,
|
||||
Name: name,
|
||||
Type: store.TypeExecutable,
|
||||
Kind: kind,
|
||||
ExecArgs: entry.ExecArgs,
|
||||
BinaryPath: binDst,
|
||||
IconPath: iconPath,
|
||||
@@ -174,6 +187,7 @@ func InstallWebApp(o WebOptions) (store.Shortcut, error) {
|
||||
Slug: sl,
|
||||
Name: name,
|
||||
Type: store.TypeWebApp,
|
||||
Kind: KindPWA,
|
||||
URL: pageURL,
|
||||
Browser: strings.Join(o.Browser.Exec, " "),
|
||||
ExecArgs: entry.ExecArgs,
|
||||
|
||||
@@ -46,10 +46,12 @@ func checkRunnable(path string) error {
|
||||
head := make([]byte, 4)
|
||||
n, _ := io.ReadFull(f, head)
|
||||
head = head[:n]
|
||||
if bytes.HasPrefix(head, []byte{0x7f, 'E', 'L', 'F'}) || bytes.HasPrefix(head, []byte("#!")) {
|
||||
if bytes.HasPrefix(head, []byte{0x7f, 'E', 'L', 'F'}) ||
|
||||
bytes.HasPrefix(head, []byte("#!")) ||
|
||||
bytes.HasPrefix(head, []byte("MZ")) { // .exe Windows (dijalankan lewat wine)
|
||||
return nil
|
||||
}
|
||||
return errors.New("not an executable binary (ELF, AppImage, or script only)")
|
||||
return errors.New("not an executable (ELF, AppImage, script, or Windows .exe)")
|
||||
}
|
||||
|
||||
// copyFile menyalin src ke dst lewat file sementara supaya dst tidak pernah setengah tertulis.
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
package shortcut
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"debug/buildinfo"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
// Jenis aplikasi yang bisa dikenali LazyRun.
|
||||
const (
|
||||
KindGo = "go"
|
||||
KindAppImage = "appimage"
|
||||
KindElectron = "electron"
|
||||
KindScript = "script"
|
||||
KindWindows = "windows"
|
||||
KindBinary = "binary"
|
||||
KindPWA = "pwa"
|
||||
)
|
||||
|
||||
// DetectKind menebak jenis berkas yang dipilih user:
|
||||
// binary Go, AppImage, aplikasi Electron, script, .exe Windows, atau binary biasa.
|
||||
func DetectKind(path string) string {
|
||||
f, err := os.Open(path)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
head := make([]byte, 12)
|
||||
n, _ := f.Read(head)
|
||||
f.Close()
|
||||
head = head[:n]
|
||||
|
||||
switch {
|
||||
case bytes.HasPrefix(head, []byte("MZ")):
|
||||
return KindWindows
|
||||
case bytes.HasPrefix(head, []byte("#!")):
|
||||
return KindScript
|
||||
case isAppImageHead(head):
|
||||
return KindAppImage
|
||||
case bytes.HasPrefix(head, []byte{0x7f, 'E', 'L', 'F'}):
|
||||
if _, err := buildinfo.ReadFile(path); err == nil {
|
||||
return KindGo
|
||||
}
|
||||
if hasElectronResources(path) {
|
||||
return KindElectron
|
||||
}
|
||||
return KindBinary
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// hasElectronResources mencari berkas penanda Electron di sebelah binary.
|
||||
func hasElectronResources(path string) bool {
|
||||
resources := filepath.Join(filepath.Dir(path), "resources")
|
||||
for _, name := range []string{"app.asar", "default_app.asar", "app"} {
|
||||
if _, err := os.Stat(filepath.Join(resources, name)); err == nil {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// isAppImageHead memeriksa magic AppImage (ELF + "AI" pada offset 8).
|
||||
// Sengaja terpisah dari internal/update supaya paket ini tidak tergantung ke sana.
|
||||
func isAppImageHead(head []byte) bool {
|
||||
return len(head) >= 11 &&
|
||||
head[0] == 0x7f && head[1] == 'E' && head[2] == 'L' && head[3] == 'F' &&
|
||||
head[8] == 'A' && head[9] == 'I' && (head[10] == 0x01 || head[10] == 0x02)
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
package shortcut
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestDetectKind(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
|
||||
write := func(name string, head []byte) string {
|
||||
path := filepath.Join(dir, name)
|
||||
if err := os.WriteFile(path, append(head, make([]byte, 64)...), 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return path
|
||||
}
|
||||
|
||||
elf := []byte{0x7f, 'E', 'L', 'F', 0x02, 0x01, 0x01, 0x00}
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
path string
|
||||
want string
|
||||
}{
|
||||
{"windows exe", write("winbox.exe", []byte("MZ\x90\x00")), KindWindows},
|
||||
{"script", write("tool.sh", []byte("#!/bin/sh\n")), KindScript},
|
||||
{"appimage", write("app.AppImage", append(append([]byte{}, elf...), 'A', 'I', 0x02, 0x00)), KindAppImage},
|
||||
{"binary biasa", write("binary", elf), KindBinary},
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
if got := DetectKind(c.path); got != c.want {
|
||||
t.Errorf("DetectKind() = %q, mau %q", got, c.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// binary Go: test ini sendiri hasil build Go, jadi harus terdeteksi "go"
|
||||
exe, err := os.Executable()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got := DetectKind(exe); got != KindGo {
|
||||
t.Errorf("DetectKind(test binary) = %q, mau %q", got, KindGo)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDetectKindElectron(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
app := filepath.Join(dir, "myapp")
|
||||
if err := os.WriteFile(app, append([]byte{0x7f, 'E', 'L', 'F', 0x02, 0x01, 0x01, 0x00}, make([]byte, 32)...), 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.MkdirAll(filepath.Join(dir, "resources"), 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(dir, "resources", "app.asar"), []byte("x"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if got := DetectKind(app); got != KindElectron {
|
||||
t.Errorf("DetectKind(electron) = %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDetectKindUnknownFile(t *testing.T) {
|
||||
path := filepath.Join(t.TempDir(), "catatan.txt")
|
||||
if err := os.WriteFile(path, []byte("halo"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got := DetectKind(path); got != "" {
|
||||
t.Errorf("teks biasa tidak punya kind, dapat %q", got)
|
||||
}
|
||||
if got := DetectKind(filepath.Join(t.TempDir(), "tidak-ada")); got != "" {
|
||||
t.Errorf("berkas tidak ada harus kosong, dapat %q", got)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user