Update default taskfile. WIP linux packaging

This commit is contained in:
Lea Anthony 2023-11-18 13:08:59 +11:00
commit 12efb8b981
No known key found for this signature in database
GPG key ID: 33DAF7BB90A58405
14 changed files with 1525 additions and 44 deletions

View file

@ -2,4 +2,4 @@
`wails init`,:material-check-bold:,:material-check-bold:,:material-check-bold:
`wails build`,:material-check-bold:,:material-check-bold:,:material-check-bold:
`wails dev`," "," "," "
`wails package`," ",:material-check-bold:," "
`wails package`," ",:material-check-bold:,:material-check-bold:

1 Mac Windows Linux
2 `wails init` :material-check-bold: :material-check-bold: :material-check-bold:
3 `wails build` :material-check-bold: :material-check-bold: :material-check-bold:
4 `wails dev`
5 `wails package` :material-check-bold: :material-check-bold:

View file

@ -45,6 +45,8 @@ func main() {
generate.NewSubCommandFunction("bindings", "Generate bindings + models", commands.GenerateBindings)
generate.NewSubCommandFunction("constants", "Generate JS constants from Go", commands.GenerateConstants)
generate.NewSubCommandFunction(".desktop", "Generate .desktop file", commands.GenerateDotDesktop)
generate.NewSubCommandFunction("appimage", "Generate Linux AppImage", commands.GenerateAppImage)
plugin := app.NewSubCommand("plugin", "Plugin tools")
//plugin.NewSubCommandFunction("list", "List plugins", commands.PluginList)
plugin.NewSubCommandFunction("init", "Initialise a new plugin", commands.PluginInit)

View file

@ -10,6 +10,7 @@ require (
github.com/go-task/task/v3 v3.31.0
github.com/godbus/dbus/v5 v5.1.0
github.com/google/go-cmp v0.5.9
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510
github.com/google/uuid v1.3.0
github.com/gorilla/pat v1.0.1
github.com/gorilla/sessions v1.2.1

View file

@ -173,6 +173,8 @@ github.com/google/pprof v0.0.0-20200905233945-acf8798be1f7/go.mod h1:ZgVRPoUq/hf
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ=
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4=
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=

View file

@ -0,0 +1,173 @@
package commands
import (
_ "embed"
"fmt"
"github.com/pterm/pterm"
"github.com/wailsapp/wails/v3/internal/s"
"os"
"path/filepath"
"sync"
)
//go:embed linuxdeploy-plugin-gtk.sh
var gtkPlugin []byte
func log(p *pterm.ProgressbarPrinter, message string) {
p.UpdateTitle(message)
pterm.Info.Println(message)
p.Increment()
}
type GenerateAppImageOptions struct {
Binary string `description:"The binary to package including path"`
Icon string `description:"Path to the icon"`
DesktopFile string `description:"Path to the desktop file"`
OutputDir string `description:"Path to the output directory" default:"."`
BuildDir string `description:"Path to the build directory"`
}
func GenerateAppImage(options *GenerateAppImageOptions) error {
defer func() {
pterm.DefaultSpinner.Stop()
}()
if options.Binary == "" {
return fmt.Errorf("binary not provided")
}
if options.Icon == "" {
return fmt.Errorf("icon path not provided")
}
if options.DesktopFile == "" {
return fmt.Errorf("desktop file path not provided")
}
if options.BuildDir == "" {
// Create temp directory
var err error
options.BuildDir, err = os.MkdirTemp("", "wails-appimage-*")
if err != nil {
return err
}
}
var err error
options.OutputDir, err = filepath.Abs(options.OutputDir)
if err != nil {
return err
}
return generateAppImage(options)
}
func generateAppImage(options *GenerateAppImageOptions) error {
numberOfSteps := 5
p, _ := pterm.DefaultProgressbar.WithTotal(numberOfSteps).WithTitle("Generating AppImage").Start()
// Get the last path of the binary and normalise the name
name := normaliseName(filepath.Base(options.Binary))
appDir := filepath.Join(options.BuildDir, name+"-x86_64.AppDir")
s.RMDIR(appDir)
log(p, "Preparing AppImage Directory: "+appDir)
usrBin := filepath.Join(appDir, "usr", "bin")
s.MKDIR(options.BuildDir)
s.MKDIR(usrBin)
s.COPY(options.Binary, usrBin)
s.CHMOD(filepath.Join(usrBin, filepath.Base(options.Binary)), 0755)
dotDirIcon := filepath.Join(appDir, ".DirIcon")
s.COPY(options.Icon, dotDirIcon)
iconLink := filepath.Join(appDir, filepath.Base(options.Icon))
s.DELETE(iconLink)
s.SYMLINK(".DirIcon", iconLink)
s.COPY(options.DesktopFile, appDir)
// Download linuxdeploy and make it executable
s.CD(options.BuildDir)
// Download necessary files
log(p, "Downloading AppImage tooling")
var wg sync.WaitGroup
wg.Add(2)
go func() {
if !s.EXISTS(filepath.Join(options.BuildDir, "linuxdeploy-x86_64.AppImage")) {
s.DOWNLOAD("https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-x86_64.AppImage", filepath.Join(options.BuildDir, "linuxdeploy-x86_64.AppImage"))
}
s.CHMOD(filepath.Join(options.BuildDir, "linuxdeploy-x86_64.AppImage"), 0755)
wg.Done()
}()
go func() {
target := filepath.Join(appDir, "AppRun")
if !s.EXISTS(target) {
s.DOWNLOAD("https://github.com/AppImage/AppImageKit/releases/download/continuous/AppRun-x86_64", target)
}
s.CHMOD(target, 0755)
wg.Done()
}()
wg.Wait()
log(p, "Processing GTK files.")
files := s.FINDFILES("/usr/lib", "WebKitNetworkProcess", "WebKitWebProcess", "libwebkit2gtkinjectedbundle.so")
if len(files) != 3 {
return fmt.Errorf("unable to locate all WebKit libraries")
}
s.CD(appDir)
for _, file := range files {
targetDir := filepath.Dir(file)
// Strip leading forward slash
if targetDir[0] == '/' {
targetDir = targetDir[1:]
}
var err error
targetDir, err = filepath.Abs(targetDir)
if err != nil {
return err
}
s.MKDIR(targetDir)
s.COPY(file, targetDir)
}
// Copy GTK Plugin
err := os.WriteFile(filepath.Join(options.BuildDir, "linuxdeploy-plugin-gtk.sh"), gtkPlugin, 0755)
if err != nil {
return err
}
// Determine GTK Version
// Run ldd on the binary and capture the output
targetBinary := filepath.Join(appDir, "usr", "bin", options.Binary)
lddOutput, err := s.EXEC(fmt.Sprintf("ldd %s", targetBinary))
if err != nil {
println(string(lddOutput))
return err
}
lddString := string(lddOutput)
// Check if GTK3 is present
var DeployGtkVersion string
if s.CONTAINS(lddString, "libgtk-x11-2.0.so") {
DeployGtkVersion = "2"
} else if s.CONTAINS(lddString, "libgtk-3.so") {
DeployGtkVersion = "3"
} else if s.CONTAINS(lddString, "libgtk-4.so") {
DeployGtkVersion = "4"
} else {
return fmt.Errorf("unable to determine GTK version")
}
// Run linuxdeploy to bundle the application
s.CD(options.BuildDir)
log(p, "Generating AppImage (This may take a while...)")
cmd := fmt.Sprintf("./linuxdeploy-x86_64.AppImage --appimage-extract-and-run --appdir %s --output appimage --plugin gtk", appDir)
s.SETENV("DEPLOY_GTK_VERSION", DeployGtkVersion)
output, err := s.EXEC(cmd)
if err != nil {
println(output)
return err
}
// Move file to output directory
targetFile := filepath.Join(options.BuildDir, name+"-x86_64.AppImage")
s.MOVE(targetFile, options.OutputDir)
log(p, "AppImage created: "+targetFile)
return nil
}

View file

@ -0,0 +1,80 @@
//go:build full_test
package commands_test
import (
"github.com/wailsapp/wails/v3/internal/commands"
"github.com/wailsapp/wails/v3/internal/s"
"testing"
)
func Test_generateAppImage(t *testing.T) {
tests := []struct {
name string
options *commands.GenerateAppImageOptions
wantErr bool
setup func()
teardown func()
}{
{
name: "Should fail if binary path is not provided",
options: &commands.GenerateAppImageOptions{},
wantErr: true,
},
{
name: "Should fail if Icon is not provided",
options: &commands.GenerateAppImageOptions{
Binary: "testapp",
},
wantErr: true,
},
{
name: "Should fail if desktop file is not provided",
options: &commands.GenerateAppImageOptions{
Binary: "testapp",
Icon: "testicon",
},
wantErr: true,
},
{
name: "Should work if inputs are valid",
options: &commands.GenerateAppImageOptions{
Binary: "testapp",
Icon: "appicon.png",
DesktopFile: "testapp.desktop",
},
setup: func() {
// Compile the test application
s.CD("appimage_testfiles")
testDir := s.CWD()
_, err := s.EXEC(`go build -ldflags="-s -w" -o testapp`)
if err != nil {
t.Fatal(err)
}
s.DEFER(func() {
s.CD(testDir)
s.RM("testapp")
s.RM("testapp-x86_64.AppImage")
})
},
teardown: func() {
s.CALLDEFER()
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.setup != nil {
tt.setup()
}
if err := commands.GenerateAppImage(tt.options); (err != nil) != tt.wantErr {
t.Errorf("generateAppImage() error = %v, wantErr %v", err, tt.wantErr)
}
if tt.teardown != nil {
tt.teardown()
}
})
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 130 KiB

View file

@ -0,0 +1,417 @@
package main
import (
_ "embed"
"fmt"
"log"
"math/rand"
"runtime"
"strconv"
"time"
"github.com/samber/lo"
"github.com/wailsapp/wails/v3/pkg/events"
"github.com/wailsapp/wails/v3/pkg/application"
)
func main() {
app := application.New(application.Options{
Name: "WebviewWindow Demo",
Description: "A demo of the WebviewWindow API",
Assets: application.AlphaAssets,
Mac: application.MacOptions{
ApplicationShouldTerminateAfterLastWindowClosed: false,
},
})
app.On(events.Mac.ApplicationDidFinishLaunching, func(event *application.Event) {
log.Println("ApplicationDidFinishLaunching")
})
var hiddenWindows []*application.WebviewWindow
currentWindow := func(fn func(window *application.WebviewWindow)) {
if app.CurrentWindow() != nil {
fn(app.CurrentWindow())
} else {
println("Current WebviewWindow is nil")
}
}
// Create a custom menu
menu := app.NewMenu()
menu.AddRole(application.AppMenu)
windowCounter := 1
// Let's make a "Demo" menu
myMenu := menu.AddSubmenu("New")
myMenu.Add("New WebviewWindow").
SetAccelerator("CmdOrCtrl+N").
OnClick(func(ctx *application.Context) {
app.NewWebviewWindow().
SetTitle("WebviewWindow "+strconv.Itoa(windowCounter)).
SetRelativePosition(rand.Intn(1000), rand.Intn(800)).
SetURL("https://wails.io").
Show()
windowCounter++
})
myMenu.Add("New WebviewWindow (Hides on Close one time)").
SetAccelerator("CmdOrCtrl+H").
OnClick(func(ctx *application.Context) {
app.NewWebviewWindowWithOptions(application.WebviewWindowOptions{
// This will be called when the user clicks the close button
// on the window. It will hide the window for 5 seconds.
// If the user clicks the close button again, the window will
// close.
ShouldClose: func(window *application.WebviewWindow) bool {
if !lo.Contains(hiddenWindows, window) {
hiddenWindows = append(hiddenWindows, window)
go func() {
time.Sleep(5 * time.Second)
window.Show()
}()
window.Hide()
return false
}
// Remove the window from the hiddenWindows list
hiddenWindows = lo.Without(hiddenWindows, window)
return true
},
}).
SetTitle("WebviewWindow "+strconv.Itoa(windowCounter)).
SetRelativePosition(rand.Intn(1000), rand.Intn(800)).
SetURL("https://wails.io").
Show()
windowCounter++
})
myMenu.Add("New Frameless WebviewWindow").
SetAccelerator("CmdOrCtrl+F").
OnClick(func(ctx *application.Context) {
app.NewWebviewWindowWithOptions(application.WebviewWindowOptions{
X: rand.Intn(1000),
Y: rand.Intn(800),
BackgroundColour: application.NewRGB(33, 37, 41),
Frameless: true,
Mac: application.MacWindow{
InvisibleTitleBarHeight: 50,
},
}).Show()
windowCounter++
})
myMenu.Add("New WebviewWindow (ignores mouse events").
SetAccelerator("CmdOrCtrl+F").
OnClick(func(ctx *application.Context) {
app.NewWebviewWindowWithOptions(application.WebviewWindowOptions{
HTML: "<div style='width: 100%; height: 95%; border: 3px solid red; background-color: \"0000\";'></div>",
X: rand.Intn(1000),
Y: rand.Intn(800),
IgnoreMouseEvents: true,
BackgroundType: application.BackgroundTypeTransparent,
Mac: application.MacWindow{
InvisibleTitleBarHeight: 50,
},
}).Show()
windowCounter++
})
if runtime.GOOS == "darwin" {
myMenu.Add("New WebviewWindow (MacTitleBarHiddenInset)").
OnClick(func(ctx *application.Context) {
app.NewWebviewWindowWithOptions(application.WebviewWindowOptions{
Mac: application.MacWindow{
TitleBar: application.MacTitleBarHiddenInset,
InvisibleTitleBarHeight: 25,
},
}).
SetBackgroundColour(application.NewRGB(33, 37, 41)).
SetTitle("WebviewWindow "+strconv.Itoa(windowCounter)).
SetRelativePosition(rand.Intn(1000), rand.Intn(800)).
SetHTML("<br/><br/><p>A MacTitleBarHiddenInset WebviewWindow example</p>").
Show()
windowCounter++
})
myMenu.Add("New WebviewWindow (MacTitleBarHiddenInsetUnified)").
OnClick(func(ctx *application.Context) {
app.NewWebviewWindowWithOptions(application.WebviewWindowOptions{
Mac: application.MacWindow{
TitleBar: application.MacTitleBarHiddenInsetUnified,
InvisibleTitleBarHeight: 50,
},
}).
SetTitle("WebviewWindow "+strconv.Itoa(windowCounter)).
SetRelativePosition(rand.Intn(1000), rand.Intn(800)).
SetHTML("<br/><br/><p>A MacTitleBarHiddenInsetUnified WebviewWindow example</p>").
Show()
windowCounter++
})
myMenu.Add("New WebviewWindow (MacTitleBarHidden)").
OnClick(func(ctx *application.Context) {
app.NewWebviewWindowWithOptions(application.WebviewWindowOptions{
Mac: application.MacWindow{
TitleBar: application.MacTitleBarHidden,
InvisibleTitleBarHeight: 25,
},
}).
SetTitle("WebviewWindow "+strconv.Itoa(windowCounter)).
SetRelativePosition(rand.Intn(1000), rand.Intn(800)).
SetHTML("<br/><br/><p>A MacTitleBarHidden WebviewWindow example</p>").
Show()
windowCounter++
})
}
if runtime.GOOS == "windows" {
myMenu.Add("New WebviewWindow (Mica)").
OnClick(func(ctx *application.Context) {
app.NewWebviewWindowWithOptions(application.WebviewWindowOptions{
Title: "WebviewWindow " + strconv.Itoa(windowCounter),
X: rand.Intn(1000),
Y: rand.Intn(800),
BackgroundType: application.BackgroundTypeTranslucent,
HTML: "<html style='background-color: rgba(0,0,0,0);'><body></body></html>",
Windows: application.WindowsWindow{
BackdropType: application.Mica,
},
}).Show()
windowCounter++
})
myMenu.Add("New WebviewWindow (Acrylic)").
OnClick(func(ctx *application.Context) {
app.NewWebviewWindowWithOptions(application.WebviewWindowOptions{
Title: "WebviewWindow " + strconv.Itoa(windowCounter),
X: rand.Intn(1000),
Y: rand.Intn(800),
BackgroundType: application.BackgroundTypeTranslucent,
HTML: "<html style='background-color: rgba(0,0,0,0);'><body></body></html>",
Windows: application.WindowsWindow{
BackdropType: application.Acrylic,
},
}).Show()
windowCounter++
})
myMenu.Add("New WebviewWindow (Tabbed)").
OnClick(func(ctx *application.Context) {
app.NewWebviewWindowWithOptions(application.WebviewWindowOptions{
Title: "WebviewWindow " + strconv.Itoa(windowCounter),
X: rand.Intn(1000),
Y: rand.Intn(800),
BackgroundType: application.BackgroundTypeTranslucent,
HTML: "<html style='background-color: rgba(0,0,0,0);'><body></body></html>",
Windows: application.WindowsWindow{
BackdropType: application.Tabbed,
},
}).Show()
windowCounter++
})
}
sizeMenu := menu.AddSubmenu("Size")
sizeMenu.Add("Set Size (800,600)").OnClick(func(ctx *application.Context) {
currentWindow(func(w *application.WebviewWindow) {
w.SetSize(800, 600)
})
})
sizeMenu.Add("Set Size (Random)").OnClick(func(ctx *application.Context) {
currentWindow(func(w *application.WebviewWindow) {
w.SetSize(rand.Intn(800)+200, rand.Intn(600)+200)
})
})
sizeMenu.Add("Set Min Size (200,200)").OnClick(func(ctx *application.Context) {
currentWindow(func(w *application.WebviewWindow) {
w.SetMinSize(200, 200)
})
})
sizeMenu.Add("Set Max Size (600,600)").OnClick(func(ctx *application.Context) {
currentWindow(func(w *application.WebviewWindow) {
w.SetFullscreenButtonEnabled(false)
w.SetMaxSize(600, 600)
})
})
sizeMenu.Add("Get Current WebviewWindow Size").OnClick(func(ctx *application.Context) {
currentWindow(func(w *application.WebviewWindow) {
width, height := w.Size()
application.InfoDialog().SetTitle("Current WebviewWindow Size").SetMessage("Width: " + strconv.Itoa(width) + " Height: " + strconv.Itoa(height)).Show()
})
})
sizeMenu.Add("Reset Min Size").OnClick(func(ctx *application.Context) {
currentWindow(func(w *application.WebviewWindow) {
w.SetMinSize(0, 0)
})
})
sizeMenu.Add("Reset Max Size").OnClick(func(ctx *application.Context) {
currentWindow(func(w *application.WebviewWindow) {
w.SetMaxSize(0, 0)
w.SetFullscreenButtonEnabled(true)
})
})
positionMenu := menu.AddSubmenu("Position")
positionMenu.Add("Set Relative Position (0,0)").OnClick(func(ctx *application.Context) {
currentWindow(func(w *application.WebviewWindow) {
w.SetRelativePosition(0, 0)
})
})
positionMenu.Add("Set Relative Position (Random)").OnClick(func(ctx *application.Context) {
currentWindow(func(w *application.WebviewWindow) {
w.SetRelativePosition(rand.Intn(1000), rand.Intn(800))
})
})
positionMenu.Add("Get Relative Position").OnClick(func(ctx *application.Context) {
currentWindow(func(w *application.WebviewWindow) {
x, y := w.RelativePosition()
application.InfoDialog().SetTitle("Current WebviewWindow Position").SetMessage("X: " + strconv.Itoa(x) + " Y: " + strconv.Itoa(y)).Show()
})
})
positionMenu.Add("Set Absolute Position (0,0)").OnClick(func(ctx *application.Context) {
currentWindow(func(w *application.WebviewWindow) {
w.SetAbsolutePosition(0, 0)
})
})
positionMenu.Add("Set Absolute Position (Random)").OnClick(func(ctx *application.Context) {
currentWindow(func(w *application.WebviewWindow) {
w.SetAbsolutePosition(rand.Intn(1000), rand.Intn(800))
})
})
positionMenu.Add("Get Absolute Position").OnClick(func(ctx *application.Context) {
currentWindow(func(w *application.WebviewWindow) {
x, y := w.AbsolutePosition()
application.InfoDialog().SetTitle("Current WebviewWindow Position").SetMessage("X: " + strconv.Itoa(x) + " Y: " + strconv.Itoa(y)).Show()
})
})
positionMenu.Add("Center").OnClick(func(ctx *application.Context) {
currentWindow(func(w *application.WebviewWindow) {
w.Center()
})
})
stateMenu := menu.AddSubmenu("State")
stateMenu.Add("Minimise (for 2 secs)").OnClick(func(ctx *application.Context) {
currentWindow(func(w *application.WebviewWindow) {
w.Minimise()
time.Sleep(2 * time.Second)
w.Restore()
})
})
stateMenu.Add("Maximise").OnClick(func(ctx *application.Context) {
currentWindow(func(w *application.WebviewWindow) {
w.Maximise()
})
})
stateMenu.Add("Fullscreen").OnClick(func(ctx *application.Context) {
currentWindow(func(w *application.WebviewWindow) {
w.Fullscreen()
})
})
stateMenu.Add("UnFullscreen").OnClick(func(ctx *application.Context) {
currentWindow(func(w *application.WebviewWindow) {
w.UnFullscreen()
})
})
stateMenu.Add("Restore").OnClick(func(ctx *application.Context) {
currentWindow(func(w *application.WebviewWindow) {
w.Restore()
})
})
stateMenu.Add("Hide (for 2 seconds)").OnClick(func(ctx *application.Context) {
currentWindow(func(w *application.WebviewWindow) {
w.Hide()
time.Sleep(2 * time.Second)
w.Show()
})
})
stateMenu.Add("Always on Top").OnClick(func(ctx *application.Context) {
currentWindow(func(w *application.WebviewWindow) {
w.SetAlwaysOnTop(true)
})
})
stateMenu.Add("Not always on Top").OnClick(func(ctx *application.Context) {
currentWindow(func(w *application.WebviewWindow) {
w.SetAlwaysOnTop(false)
})
})
stateMenu.Add("Google.com").OnClick(func(ctx *application.Context) {
currentWindow(func(w *application.WebviewWindow) {
w.SetURL("https://google.com")
})
})
stateMenu.Add("wails.io").OnClick(func(ctx *application.Context) {
currentWindow(func(w *application.WebviewWindow) {
w.SetURL("https://wails.io")
})
})
stateMenu.Add("Get Primary Screen").OnClick(func(ctx *application.Context) {
screen, err := app.GetPrimaryScreen()
if err != nil {
application.ErrorDialog().SetTitle("Error").SetMessage(err.Error()).Show()
return
}
msg := fmt.Sprintf("Screen: %+v", screen)
application.InfoDialog().SetTitle("Primary Screen").SetMessage(msg).Show()
})
stateMenu.Add("Get Screens").OnClick(func(ctx *application.Context) {
screens, err := app.GetScreens()
if err != nil {
application.ErrorDialog().SetTitle("Error").SetMessage(err.Error()).Show()
return
}
for _, screen := range screens {
msg := fmt.Sprintf("Screen: %+v", screen)
application.InfoDialog().SetTitle(fmt.Sprintf("Screen %s", screen.ID)).SetMessage(msg).Show()
}
})
stateMenu.Add("Get Screen for WebviewWindow").OnClick(func(ctx *application.Context) {
currentWindow(func(w *application.WebviewWindow) {
screen, err := w.GetScreen()
if err != nil {
application.ErrorDialog().SetTitle("Error").SetMessage(err.Error()).Show()
return
}
msg := fmt.Sprintf("Screen: %+v", screen)
application.InfoDialog().SetTitle(fmt.Sprintf("Screen %s", screen.ID)).SetMessage(msg).Show()
})
})
stateMenu.Add("Disable for 5s").OnClick(func(ctx *application.Context) {
currentWindow(func(w *application.WebviewWindow) {
w.SetEnabled(false)
time.Sleep(5 * time.Second)
w.SetEnabled(true)
})
})
if runtime.GOOS == "windows" {
stateMenu.Add("Flash Start").OnClick(func(ctx *application.Context) {
currentWindow(func(w *application.WebviewWindow) {
time.Sleep(2 * time.Second)
w.Flash(true)
})
})
}
printMenu := menu.AddSubmenu("Print")
printMenu.Add("Print").OnClick(func(ctx *application.Context) {
currentWindow(func(w *application.WebviewWindow) {
_ = w.Print()
})
})
app.NewWebviewWindowWithOptions(application.WebviewWindowOptions{
BackgroundColour: application.NewRGB(33, 37, 41),
Mac: application.MacWindow{
DisableShadow: true,
},
})
app.SetMenu(menu)
err := app.Run()
if err != nil {
log.Fatal(err)
}
}

View file

@ -0,0 +1,10 @@
[Desktop Entry]
Type=Application
Name=testapp
Exec=testapp
Icon=appicon
Categories=Development;
Terminal=false
Keywords=wails
Version=1.0
StartupNotify=false

View file

@ -35,9 +35,7 @@ func (d *DotDesktopOptions) asBytes() []byte {
if d.Icon != "" {
buf.WriteString(fmt.Sprintf("Icon=%s\n", d.Icon))
}
if d.Categories != "" {
buf.WriteString(fmt.Sprintf("Categories=%s\n", d.Categories))
}
buf.WriteString(fmt.Sprintf("Categories=%s\n", d.Categories))
if d.Comment != "" {
buf.WriteString(fmt.Sprintf("Comment=%s\n", d.Comment))
}

View file

@ -0,0 +1,376 @@
#! /usr/bin/env bash
# Source: https://raw.githubusercontent.com/linuxdeploy/linuxdeploy-plugin-gtk/master/linuxdeploy-plugin-gtk.sh
# License: MIT (https://github.com/linuxdeploy/linuxdeploy-plugin-gtk/blob/master/LICENSE.txt)
# GTK3 environment variables: https://developer.gnome.org/gtk3/stable/gtk-running.html
# GTK4 environment variables: https://developer.gnome.org/gtk4/stable/gtk-running.html
# abort on all errors
set -e
if [ "$DEBUG" != "" ]; then
set -x
verbose="--verbose"
fi
SCRIPT="$(basename "$(readlink -f "$0")")"
show_usage() {
echo "Usage: $SCRIPT --appdir <path to AppDir>"
echo
echo "Bundles resources for applications that use GTK into an AppDir"
echo
echo "Required variables:"
echo " LINUXDEPLOY=\".../linuxdeploy\" path to linuxdeploy (e.g., AppImage); set automatically when plugin is run directly by linuxdeploy"
echo
echo "Optional variables:"
echo " DEPLOY_GTK_VERSION (major version of GTK to deploy, e.g. '2', '3' or '4'; auto-detect by default)"
}
variable_is_true() {
local var="$1"
if [ -n "$var" ] && { [ "$var" == "true" ] || [ "$var" -gt 0 ]; } 2> /dev/null; then
return 0 # true
else
return 1 # false
fi
}
get_pkgconf_variable() {
local variable="$1"
local library="$2"
local default_value="$3"
pkgconfig_ret="$("$PKG_CONFIG" --variable="$variable" "$library")"
if [ -n "$pkgconfig_ret" ]; then
echo "$pkgconfig_ret"
elif [ -n "$default_value" ]; then
echo "$default_value"
else
echo "$0: there is no '$variable' variable for '$library' library." > /dev/stderr
echo "Please check the '$library.pc' file is present in \$PKG_CONFIG_PATH (you may need to install the appropriate -dev/-devel package)." > /dev/stderr
exit 1
fi
}
copy_tree() {
local src=("${@:1:$#-1}")
local dst="${*:$#}"
for elem in "${src[@]}"; do
mkdir -p "${dst::-1}$elem"
cp "$elem" --archive --parents --target-directory="$dst" $verbose
done
}
copy_lib_tree() {
# The source lib directory could be /usr/lib, /usr/lib64, or /usr/lib/x86_64-linux-gnu
# Therefore, when copying lib directories, we need to transform that target path
# to a consistent /usr/lib
local src=("${@:1:$#-1}")
local dst="${*:$#}"
for elem in "${src[@]}"; do
mkdir -p "${dst::-1}${elem/$LD_GTK_LIBRARY_PATH//usr/lib}"
pushd "$LD_GTK_LIBRARY_PATH"
cp "$(realpath --relative-to="$LD_GTK_LIBRARY_PATH" "$elem")" --archive --parents --target-directory="$dst/usr/lib" $verbose
popd
done
}
get_triplet_path() {
if command -v dpkg-architecture > /dev/null; then
echo "/usr/lib/$(dpkg-architecture -qDEB_HOST_MULTIARCH)"
fi
}
search_library_path() {
PATH_ARRAY=(
"$(get_triplet_path)"
"/usr/lib64"
"/usr/lib"
)
for path in "${PATH_ARRAY[@]}"; do
if [ -d "$path" ]; then
echo "$path"
return 0
fi
done
}
search_tool() {
local tool="$1"
local directory="$2"
if command -v "$tool"; then
return 0
fi
PATH_ARRAY=(
"$(get_triplet_path)/$directory/$tool"
"/usr/lib64/$directory/$tool"
"/usr/lib/$directory/$tool"
"/usr/bin/$tool"
"/usr/bin/$tool-64"
"/usr/bin/$tool-32"
)
for path in "${PATH_ARRAY[@]}"; do
if [ -x "$path" ]; then
echo "$path"
return 0
fi
done
}
DEPLOY_GTK_VERSION="${DEPLOY_GTK_VERSION:-0}" # When not set by user, this variable use the integer '0' as a sentinel value
APPDIR=
while [ "$1" != "" ]; do
case "$1" in
--plugin-api-version)
echo "0"
exit 0
;;
--appdir)
APPDIR="$2"
shift
shift
;;
--help)
show_usage
exit 0
;;
*)
echo "Invalid argument: $1"
echo
show_usage
exit 1
;;
esac
done
if [ "$APPDIR" == "" ]; then
show_usage
exit 1
fi
APPDIR="$(realpath "$APPDIR")"
mkdir -p "$APPDIR"
. /etc/os-release
if [ "$ID" = "debian" ] || [ "$ID" = "ubuntu" ]; then
if ! command -v dpkg-architecture &>/dev/null; then
echo -e "$0: dpkg-architecture not found.\nInstall dpkg-dev then re-run the plugin."
exit 1
fi
fi
if command -v pkgconf > /dev/null; then
PKG_CONFIG="pkgconf"
elif command -v pkg-config > /dev/null; then
PKG_CONFIG="pkg-config"
else
echo "$0: pkg-config/pkgconf not found in PATH, aborting"
exit 1
fi
# GTK's library path *must not* have a trailing slash for later parameter substitution to work properly
LD_GTK_LIBRARY_PATH="$(realpath "${LD_GTK_LIBRARY_PATH:-$(search_library_path)}")"
if ! command -v find &>/dev/null && ! type find &>/dev/null; then
echo -e "$0: find not found.\nInstall findutils then re-run the plugin."
exit 1
fi
if [ -z "$LINUXDEPLOY" ]; then
echo -e "$0: LINUXDEPLOY environment variable is not set.\nDownload a suitable linuxdeploy AppImage, set the environment variable and re-run the plugin."
exit 1
fi
gtk_versions=0 # Count major versions of GTK when auto-detect GTK version
if [ "$DEPLOY_GTK_VERSION" -eq 0 ]; then
echo "Determining which GTK version to deploy"
while IFS= read -r -d '' file; do
if [ "$DEPLOY_GTK_VERSION" -ne 2 ] && ldd "$file" | grep -q "libgtk-x11-2.0.so"; then
DEPLOY_GTK_VERSION=2
gtk_versions="$((gtk_versions+1))"
fi
if [ "$DEPLOY_GTK_VERSION" -ne 3 ] && ldd "$file" | grep -q "libgtk-3.so"; then
DEPLOY_GTK_VERSION=3
gtk_versions="$((gtk_versions+1))"
fi
if [ "$DEPLOY_GTK_VERSION" -ne 4 ] && ldd "$file" | grep -q "libgtk-4.so"; then
DEPLOY_GTK_VERSION=4
gtk_versions="$((gtk_versions+1))"
fi
done < <(find "$APPDIR/usr/bin" -executable -type f -print0)
fi
if [ "$gtk_versions" -gt 1 ]; then
echo "$0: can not deploy multiple GTK versions at the same time."
echo "Please set DEPLOY_GTK_VERSION to {2, 3, 4}."
exit 1
elif [ "$DEPLOY_GTK_VERSION" -eq 0 ]; then
echo "$0: failed to auto-detect GTK version."
echo "Please set DEPLOY_GTK_VERSION to {2, 3, 4}."
exit 1
fi
echo "Installing AppRun hook"
HOOKSDIR="$APPDIR/apprun-hooks"
HOOKFILE="$HOOKSDIR/linuxdeploy-plugin-gtk.sh"
mkdir -p "$HOOKSDIR"
cat > "$HOOKFILE" <<\EOF
#! /usr/bin/env bash
COLOR_SCHEME="$(dbus-send --session --dest=org.freedesktop.portal.Desktop --type=method_call --print-reply --reply-timeout=1000 /org/freedesktop/portal/desktop org.freedesktop.portal.Settings.Read 'string:org.freedesktop.appearance' 'string:color-scheme' 2> /dev/null | tail -n1 | cut -b35- | cut -d' ' -f2 || printf '')"
if [ -z "$COLOR_SCHEME" ]; then
COLOR_SCHEME="$(gsettings get org.gnome.desktop.interface color-scheme 2> /dev/null || printf '')"
fi
case "$COLOR_SCHEME" in
"1"|"'prefer-dark'") GTK_THEME_VARIANT="dark";;
"2"|"'prefer-light'") GTK_THEME_VARIANT="light";;
*) GTK_THEME_VARIANT="light";;
esac
APPIMAGE_GTK_THEME="${APPIMAGE_GTK_THEME:-"Adwaita:$GTK_THEME_VARIANT"}" # Allow user to override theme (discouraged)
export APPDIR="${APPDIR:-"$(dirname "$(realpath "$0")")"}" # Workaround to run extracted AppImage
export GTK_DATA_PREFIX="$APPDIR"
export GTK_THEME="$APPIMAGE_GTK_THEME" # Custom themes are broken
export GDK_BACKEND=x11 # Crash with Wayland backend on Wayland
export XDG_DATA_DIRS="$APPDIR/usr/share:/usr/share:$XDG_DATA_DIRS" # g_get_system_data_dirs() from GLib
EOF
echo "Installing GLib schemas"
# Note: schemasdir is undefined on Ubuntu 16.04
glib_schemasdir="$(get_pkgconf_variable "schemasdir" "gio-2.0" "/usr/share/glib-2.0/schemas")"
copy_tree "$glib_schemasdir" "$APPDIR/"
glib-compile-schemas "$APPDIR/$glib_schemasdir"
cat >> "$HOOKFILE" <<EOF
export GSETTINGS_SCHEMA_DIR="\$APPDIR/$glib_schemasdir"
EOF
echo "Installing GIRepository Typelibs"
gi_typelibsdir="$(get_pkgconf_variable "typelibdir" "gobject-introspection-1.0" "$LD_GTK_LIBRARY_PATH/girepository-1.0")"
copy_lib_tree "$gi_typelibsdir" "$APPDIR/"
cat >> "$HOOKFILE" <<EOF
export GI_TYPELIB_PATH="\$APPDIR/${gi_typelibsdir/$LD_GTK_LIBRARY_PATH//usr/lib}"
EOF
case "$DEPLOY_GTK_VERSION" in
2)
# https://github.com/linuxdeploy/linuxdeploy-plugin-gtk/pull/20#issuecomment-826354261
echo "WARNING: Gtk+2 applications are not fully supported by this plugin"
;;
3)
echo "Installing GTK 3.0 modules"
gtk3_exec_prefix="$(get_pkgconf_variable "exec_prefix" "gtk+-3.0" "/usr")"
gtk3_libdir="$(get_pkgconf_variable "libdir" "gtk+-3.0" "$LD_GTK_LIBRARY_PATH")/gtk-3.0"
gtk3_path="$gtk3_libdir"
gtk3_immodulesdir="$gtk3_libdir/$(get_pkgconf_variable "gtk_binary_version" "gtk+-3.0" "3.0.0")/immodules"
gtk3_printbackendsdir="$gtk3_libdir/$(get_pkgconf_variable "gtk_binary_version" "gtk+-3.0" "3.0.0")/printbackends"
gtk3_immodules_cache_file="$(dirname "$gtk3_immodulesdir")/immodules.cache"
gtk3_immodules_query="$(search_tool "gtk-query-immodules-3.0" "libgtk-3-0")"
copy_lib_tree "$gtk3_libdir" "$APPDIR/"
cat >> "$HOOKFILE" <<EOF
export GTK_EXE_PREFIX="\$APPDIR/$gtk3_exec_prefix"
export GTK_PATH="\$APPDIR/${gtk3_path/$LD_GTK_LIBRARY_PATH//usr/lib}"
export GTK_IM_MODULE_FILE="\$APPDIR/${gtk3_immodules_cache_file/$LD_GTK_LIBRARY_PATH//usr/lib}"
EOF
if [ -x "$gtk3_immodules_query" ]; then
echo "Updating immodules cache in $APPDIR/${gtk3_immodules_cache_file/$LD_GTK_LIBRARY_PATH//usr/lib}"
"$gtk3_immodules_query" > "$APPDIR/${gtk3_immodules_cache_file/$LD_GTK_LIBRARY_PATH//usr/lib}"
else
echo "WARNING: gtk-query-immodules-3.0 not found"
fi
if [ ! -f "$APPDIR/${gtk3_immodules_cache_file/$LD_GTK_LIBRARY_PATH//usr/lib}" ]; then
echo "WARNING: immodules.cache file is missing"
fi
sed -i "s|$gtk3_libdir/3.0.0/immodules/||g" "$APPDIR/${gtk3_immodules_cache_file/$LD_GTK_LIBRARY_PATH//usr/lib}"
;;
4)
echo "Installing GTK 4.0 modules"
gtk4_exec_prefix="$(get_pkgconf_variable "exec_prefix" "gtk4" "/usr")"
gtk4_libdir="$(get_pkgconf_variable "libdir" "gtk4")/gtk-4.0"
gtk4_path="$gtk4_libdir"
copy_lib_tree "$gtk4_libdir" "$APPDIR/"
cat >> "$HOOKFILE" <<EOF
export GTK_EXE_PREFIX="\$APPDIR/$gtk4_exec_prefix"
export GTK_PATH="\$APPDIR/${gtk4_path/$LD_GTK_LIBRARY_PATH//usr/lib}"
EOF
;;
*)
echo "$0: '$DEPLOY_GTK_VERSION' is not a valid GTK major version."
echo "Please set DEPLOY_GTK_VERSION to {2, 3, 4}."
exit 1
esac
echo "Installing GDK PixBufs"
gdk_libdir="$(get_pkgconf_variable "libdir" "gdk-pixbuf-2.0" "$LD_GTK_LIBRARY_PATH")"
gdk_pixbuf_binarydir="$(get_pkgconf_variable "gdk_pixbuf_binarydir" "gdk-pixbuf-2.0" "$gdk_libdir/gdk-pixbuf-2.0/2.10.0")"
gdk_pixbuf_cache_file="$(get_pkgconf_variable "gdk_pixbuf_cache_file" "gdk-pixbuf-2.0" "$gdk_pixbuf_binarydir/loaders.cache")"
gdk_pixbuf_moduledir="$(get_pkgconf_variable "gdk_pixbuf_moduledir" "gdk-pixbuf-2.0" "$gdk_pixbuf_binarydir/loaders")"
# Note: gdk_pixbuf_query_loaders variable is not defined on some systems
gdk_pixbuf_query="$(search_tool "gdk-pixbuf-query-loaders" "gdk-pixbuf-2.0")"
copy_lib_tree "$gdk_pixbuf_binarydir" "$APPDIR/"
cat >> "$HOOKFILE" <<EOF
export GDK_PIXBUF_MODULE_FILE="\$APPDIR/${gdk_pixbuf_cache_file/$LD_GTK_LIBRARY_PATH//usr/lib}"
EOF
if [ -x "$gdk_pixbuf_query" ]; then
echo "Updating pixbuf cache in $APPDIR/${gdk_pixbuf_cache_file/$LD_GTK_LIBRARY_PATH//usr/lib}"
"$gdk_pixbuf_query" > "$APPDIR/${gdk_pixbuf_cache_file/$LD_GTK_LIBRARY_PATH//usr/lib}"
else
echo "WARNING: gdk-pixbuf-query-loaders not found"
fi
if [ ! -f "$APPDIR/${gdk_pixbuf_cache_file/$LD_GTK_LIBRARY_PATH//usr/lib}" ]; then
echo "WARNING: loaders.cache file is missing"
fi
sed -i "s|$gdk_pixbuf_moduledir/||g" "$APPDIR/${gdk_pixbuf_cache_file/$LD_GTK_LIBRARY_PATH//usr/lib}"
echo "Copying more libraries"
gobject_libdir="$(get_pkgconf_variable "libdir" "gobject-2.0" "$LD_GTK_LIBRARY_PATH")"
gio_libdir="$(get_pkgconf_variable "libdir" "gio-2.0" "$LD_GTK_LIBRARY_PATH")"
librsvg_libdir="$(get_pkgconf_variable "libdir" "librsvg-2.0" "$LD_GTK_LIBRARY_PATH")"
pango_libdir="$(get_pkgconf_variable "libdir" "pango" "$LD_GTK_LIBRARY_PATH")"
pangocairo_libdir="$(get_pkgconf_variable "libdir" "pangocairo" "$LD_GTK_LIBRARY_PATH")"
pangoft2_libdir="$(get_pkgconf_variable "libdir" "pangoft2" "$LD_GTK_LIBRARY_PATH")"
FIND_ARRAY=(
"$gdk_libdir" "libgdk_pixbuf-*.so*"
"$gobject_libdir" "libgobject-*.so*"
"$gio_libdir" "libgio-*.so*"
"$librsvg_libdir" "librsvg-*.so*"
"$pango_libdir" "libpango-*.so*"
"$pangocairo_libdir" "libpangocairo-*.so*"
"$pangoft2_libdir" "libpangoft2-*.so*"
)
LIBRARIES=()
for (( i=0; i<${#FIND_ARRAY[@]}; i+=2 )); do
directory=${FIND_ARRAY[i]}
library=${FIND_ARRAY[i+1]}
while IFS= read -r -d '' file; do
LIBRARIES+=( "--library=$file" )
done < <(find "$directory" \( -type l -o -type f \) -name "$library" -print0)
done
env LINUXDEPLOY_PLUGIN_MODE=1 "$LINUXDEPLOY" --appdir="$APPDIR" "${LIBRARIES[@]}"
# Create symbolic links as a workaround
# Details: https://github.com/linuxdeploy/linuxdeploy-plugin-gtk/issues/24#issuecomment-1030026529
echo "Manually setting rpath for GTK modules"
PATCH_ARRAY=(
"$gtk3_immodulesdir"
"$gtk3_printbackendsdir"
"$gdk_pixbuf_moduledir"
)
for directory in "${PATCH_ARRAY[@]}"; do
while IFS= read -r -d '' file; do
ln $verbose -sf "${file/$LD_GTK_LIBRARY_PATH\//}" "$APPDIR/usr/lib"
done < <(find "$directory" -name '*.so' -print0)
done

View file

@ -4,7 +4,6 @@ import (
"context"
"fmt"
"github.com/go-task/task/v3/errors"
"log"
"os"
"path/filepath"
"strings"
@ -21,6 +20,11 @@ import (
// BuildSettings contains the CLI build settings
var BuildSettings = map[string]string{}
func fatal(message string) {
pterm.Error.Println(message)
os.Exit(1)
}
type RunTaskOptions struct {
Name string `pos:"1"`
Help bool `name:"h" description:"shows Task usage"`
@ -111,7 +115,7 @@ func RunTask(options *RunTaskOptions, otherArgs []string) error {
var listOptions = task.NewListOptions(options.List, options.ListAll, options.ListJSON)
if err := listOptions.Validate(); err != nil {
log.Fatal(err)
fatal(err.Error())
}
if (listOptions.ShouldListTasks()) && options.Silent {
@ -120,7 +124,7 @@ func RunTask(options *RunTaskOptions, otherArgs []string) error {
}
if err := e.Setup(); err != nil {
log.Fatal(err)
fatal(err.Error())
}
if listOptions.ShouldListTasks() {

417
v3/internal/s/s.go Normal file
View file

@ -0,0 +1,417 @@
package s
import (
"crypto/md5"
"fmt"
"github.com/google/shlex"
"io"
"net/http"
"os"
"os/exec"
"path/filepath"
"strings"
)
var (
Output io.Writer = io.Discard
IndentSize int
originalOutput io.Writer
currentIndent int
dryRun bool
deferred []func()
)
func checkError(err error) {
if err != nil {
println("\nERROR:", err.Error())
os.Exit(1)
}
}
func mute() {
originalOutput = Output
Output = io.Discard
}
func unmute() {
Output = originalOutput
}
func indent() {
currentIndent += IndentSize
}
func unindent() {
currentIndent -= IndentSize
}
func log(message string, args ...interface{}) {
indent := strings.Repeat(" ", currentIndent)
_, err := fmt.Fprintf(Output, indent+message+"\n", args...)
checkError(err)
}
// RENAME a file or directory
func RENAME(source string, target string) {
log("RENAME %s -> %s", source, target)
err := os.Rename(source, target)
checkError(err)
}
// MUSTDELETE a file.
func MUSTDELETE(filename string) {
log("DELETE %s", filename)
err := os.Remove(filepath.Join(CWD(), filename))
checkError(err)
}
// DELETE a file.
func DELETE(filename string) {
log("DELETE %s", filename)
_ = os.Remove(filepath.Join(CWD(), filename))
}
func CONTAINS(list string, item string) bool {
result := strings.Contains(list, item)
listTrimmed := list
if len(listTrimmed) > 30 {
listTrimmed = listTrimmed[:30] + "..."
}
log("CONTAINS %s in %s: %t", item, listTrimmed, result)
return result
}
func SETENV(key string, value string) {
log("SETENV %s=%s", key, value)
err := os.Setenv(key, value)
checkError(err)
}
func CD(dir string) {
err := os.Chdir(dir)
checkError(err)
log("CD %s", dir)
}
func MKDIR(path string, mode ...os.FileMode) {
var perms os.FileMode
perms = 0755
if len(mode) == 1 {
perms = mode[0]
}
log("MKDIR %s (perms: %v)", path, perms)
err := os.MkdirAll(path, perms)
checkError(err)
}
// ENDIR ensures that the path gets created if it doesn't exist
func ENDIR(path string, mode ...os.FileMode) {
var perms os.FileMode
perms = 0755
if len(mode) == 1 {
perms = mode[0]
}
_ = os.MkdirAll(path, perms)
}
// COPYDIR recursively copies a directory tree, attempting to preserve permissions.
// Source directory must exist, destination directory must *not* exist.
// Symlinks are ignored and skipped.
// Credit: https://gist.github.com/r0l1/92462b38df26839a3ca324697c8cba04
func COPYDIR(src string, dst string) {
log("COPYDIR %s -> %s", src, dst)
src = filepath.Clean(src)
dst = filepath.Clean(dst)
si, err := os.Stat(src)
checkError(err)
if !si.IsDir() {
checkError(fmt.Errorf("source is not a directory"))
}
_, err = os.Stat(dst)
if err != nil && !os.IsNotExist(err) {
checkError(err)
}
if err == nil {
checkError(fmt.Errorf("destination already exists"))
}
indent()
MKDIR(dst)
entries, err := os.ReadDir(src)
checkError(err)
for _, entry := range entries {
srcPath := filepath.Join(src, entry.Name())
dstPath := filepath.Join(dst, entry.Name())
if entry.IsDir() {
COPYDIR(srcPath, dstPath)
} else {
// Skip symlinks.
if entry.Type()&os.ModeSymlink != 0 {
continue
}
COPY(srcPath, dstPath)
}
}
unindent()
}
func SYMLINK(source string, target string) {
// trim string to first 30 chars
var trimTarget = target
if len(trimTarget) > 30 {
trimTarget = trimTarget[:30] + "..."
}
log("SYMLINK %s -> %s", source, trimTarget)
err := os.Symlink(source, target)
checkError(err)
}
// COPY file from source to target
func COPY(source string, target string) {
log("COPY %s -> %s", source, target)
src, err := os.Open(source)
checkError(err)
defer closefile(src)
if ISDIR(target) {
target = filepath.Join(target, filepath.Base(source))
}
d, err := os.Create(target)
checkError(err)
_, err = io.Copy(d, src)
checkError(err)
}
// Move file from source to target
func MOVE(source string, target string) {
// If target is a directory, append the source filename
if ISDIR(target) {
target = filepath.Join(target, filepath.Base(source))
}
log("MOVE %s -> %s", source, target)
err := os.Rename(source, target)
checkError(err)
}
func CWD() string {
result, err := os.Getwd()
checkError(err)
log("CWD %s", result)
return result
}
func RMDIR(target string) {
log("RMDIR %s", target)
err := os.RemoveAll(target)
checkError(err)
}
func RM(target string) {
log("RM %s", target)
err := os.Remove(target)
checkError(err)
}
func ECHO(message string) {
println(message)
}
func TOUCH(filepath string) {
log("TOUCH %s", filepath)
f, err := os.Create(filepath)
checkError(err)
closefile(f)
}
func EXEC(command string) ([]byte, error) {
log("EXEC %s", command)
// Split input using shlex
args, err := shlex.Split(command)
checkError(err)
// Execute command
cmd := exec.Command(args[0], args[1:]...)
cmd.Dir = CWD()
cmd.Env = os.Environ()
return cmd.CombinedOutput()
}
func CHMOD(path string, mode os.FileMode) {
log("CHMOD %s %v", path, mode)
err := os.Chmod(path, mode)
checkError(err)
}
// EXISTS - Returns true if the given path exists
func EXISTS(path string) bool {
_, err := os.Lstat(path)
log("EXISTS %s -> %t", path, err == nil)
return err == nil
}
// ISDIR returns true if the given directory exists
func ISDIR(path string) bool {
fi, err := os.Lstat(path)
if err != nil {
return false
}
return fi.Mode().IsDir()
}
// ISDIREMPTY returns true if the given directory is empty
func ISDIREMPTY(dir string) bool {
// CREDIT: https://stackoverflow.com/a/30708914/8325411
f, err := os.Open(dir)
checkError(err)
defer closefile(f)
_, err = f.Readdirnames(1) // Or f.Readdir(1)
if err == io.EOF {
return true
}
return false
}
// ISFILE returns true if the given file exists
func ISFILE(path string) bool {
fi, err := os.Lstat(path)
if err != nil {
return false
}
return fi.Mode().IsRegular()
}
// SUBDIRS returns a list of subdirectories for the given directory
func SUBDIRS(rootDir string) []string {
var result []string
// Iterate root dir
err := filepath.Walk(rootDir, func(path string, info os.FileInfo, err error) error {
checkError(err)
// If we have a directory, save it
if info.IsDir() {
result = append(result, path)
}
return nil
})
checkError(err)
return result
}
// SAVESTRING will create a file with the given string
func SAVESTRING(filename string, data string) {
log("SAVESTRING %s", filename)
mute()
SAVEBYTES(filename, []byte(data))
unmute()
}
// LOADSTRING returns the contents of the given filename as a string
func LOADSTRING(filename string) string {
log("LOADSTRING %s", filename)
mute()
data := LOADBYTES(filename)
unmute()
return string(data)
}
// SAVEBYTES will create a file with the given string
func SAVEBYTES(filename string, data []byte) {
log("SAVEBYTES %s", filename)
err := os.WriteFile(filename, data, 0755)
checkError(err)
}
// LOADBYTES returns the contents of the given filename as a string
func LOADBYTES(filename string) []byte {
log("LOADBYTES %s", filename)
data, err := os.ReadFile(filename)
checkError(err)
return data
}
func closefile(f *os.File) {
err := f.Close()
checkError(err)
}
// MD5FILE returns the md5sum of the given file
func MD5FILE(filename string) string {
f, err := os.Open(filename)
checkError(err)
defer closefile(f)
h := md5.New()
_, err = io.Copy(h, f)
checkError(err)
return fmt.Sprintf("%x", h.Sum(nil))
}
// Sub is the substitution type
type Sub map[string]string
// REPLACEALL replaces all substitution keys with associated values in the given file
func REPLACEALL(filename string, substitutions Sub) {
log("REPLACEALL %s (%v)", filename, substitutions)
data := LOADSTRING(filename)
for old, newText := range substitutions {
data = strings.ReplaceAll(data, old, newText)
}
SAVESTRING(filename, data)
}
func DOWNLOAD(url string, target string) {
log("DOWNLOAD %s -> %s", url, target)
// create HTTP client
resp, err := http.Get(url)
checkError(err)
defer resp.Body.Close()
out, err := os.Create(target)
checkError(err)
defer out.Close()
// Write the body to file
_, err = io.Copy(out, resp.Body)
checkError(err)
}
func FINDFILES(root string, filenames ...string) []string {
var result []string
// Walk the root directory trying to find all the files
err := filepath.Walk(root, func(path string, info os.FileInfo, err error) error {
checkError(err)
// If we have a file, check if it is in the list
if info.Mode().IsRegular() {
for _, filename := range filenames {
if info.Name() == filename {
result = append(result, path)
}
}
}
return nil
})
checkError(err)
log("FINDFILES in %s -> [%v]", root, strings.Join(result, ", "))
return result
}
func DEFER(fn func()) {
log("DEFER")
deferred = append(deferred, fn)
}
func CALLDEFER() {
log("CALLDEFER")
for _, fn := range deferred {
fn()
}
}

View file

@ -287,6 +287,7 @@ tasks:
create:appimage:
summary: Creates an AppImage
dir: build/appimage
platforms: [ linux ]
deps:
- task: build:linux
@ -294,44 +295,44 @@ tasks:
PRODUCTION: "true"
- task: generate:linux:dotdesktop
cmds:
- chmod +x {{ "{{.BIN_DIR}}" }}/{{ "{{.APP_NAME}}" }}
- chmod +x build/appimage/build.sh
- sh: build/appimage/build.sh
env:
- APP_NAME: {{ "'{{.APP_NAME}}'" }}
- APP_BINARY: '{{ "{{.ROOT_DIR}}"}}/{{ "{{.BIN_DIR}}" }}/{{ "{{.APP_NAME}}" }}'
- ICON_PATH: '{{ "{{.ROOT_DIR}}"}}/build/appicon.png'
- DESKTOP_FILE: '{{ "{{.ROOT_DIR}}"}}/build/{{ "{{.APP_NAME}}" }}.desktop'
# Copy binary + icon to appimage dir
- cp {{ "{{.APP_BINARY}}" }} {{ "{{.APP_NAME}}" }}
- cp ../appicon.png appicon.png
# Generate AppImage
- wails3 generate appimage -binary {{ "{{.APP_NAME}}" }} -icon {{ "{{.ICON}}" }} -desktopfile {{ "{{.DESKTOP_FILE}}" }} -outputdir {{ "{{.OUTPUT_DIR}}" }} -builddir {{ "{{.ROOT_DIR}}" }}/build/appimage
vars:
APP_NAME: '{{ "{{.APP_NAME}}" }}'
APP_BINARY: '../../bin/{{ "{{.APP_NAME}}" }}'
ICON: '../appicon.png'
DESKTOP_FILE: '{{ "{{.APP_NAME}}" }}.desktop'
OUTPUT_DIR: '../../bin'
generate:linux:dotdesktop:
summary: Generates a `.desktop` file
dir: build
sources:
- "appicon.png"
generates:
- "{{ "{{.ROOT_DIR}}"}}/build/appimage/{{ "{{.APP_NAME}}" }}.desktop"
cmds:
# Run `wails3 generate .desktop -help` for all the options
- |
wails3 generate .desktop
-name "{{ "{{.APP_NAME}}" }}"
-exec "{{ "{{.EXEC}}" }}"
-icon "{{ "{{.ICON}}" }}"
-outputfile {{ "{{.ROOT_DIR}}"}}/build/appimage/{{ "{{.APP_NAME}}" }}.desktop
# -categories "Development;Graphics;"
# -comment "A comment"
# -terminal "true"
# -version "1.0"
# -genericname "Generic Name"
# -keywords "keyword1;keyword2;"
# -startupnotify "true"
# -mimetype "application/x-extension1;application/x-extension2;"
generate:linux:dotdesktop:
summary: Generates a `.desktop` file
dir: build
sources:
- "appicon.png"
generates:
- '{{ "{{.ROOT_DIR}}"}}/build/appimage/{{ "{{.APP_NAME}}" }}.desktop'
cmds:
- mkdir -p {{ "{{.ROOT_DIR}}"}}/build/appimage
# Run `wails3 generate .desktop -help` for all the options
- wails3 generate .desktop -name "{{ "{{.APP_NAME}}" }}" -exec "{{ "{{.EXEC}}" }}" -icon "{{ "{{.ICON}}" }}" -outputfile {{ "{{.ROOT_DIR}}"}}/build/appimage/{{ "{{.APP_NAME}}" }}.desktop -categories "{{ "{{.CATEGORIES}}" }}"
# -comment "A comment"
# -terminal "true"
# -version "1.0"
# -genericname "Generic Name"
# -keywords "keyword1;keyword2;"
# -startupnotify "true"
# -mimetype "application/x-extension1;application/x-extension2;"
vars:
APP_NAME: '{{ "{{.APP_NAME}}" }}'
EXEC: '{{ "{{.APP_NAME}}" }}'
ICON: 'appicon'
CATEGORIES: 'Development;'
OUTPUTFILE: '{{ "{{.ROOT_DIR}}"}}/build/appimage/{{ "{{.APP_NAME}}" }}.desktop'
vars:
APP_NAME: '-name \"{{ "{{.APP_NAME}}" }}\"'
EXEC: '{{ "{{.ROOT_DIR}}"}}/{{ "{{.BIN_DIR}}" }}/{{ "{{.APP_NAME}}" }}'
ICON: '{{ "{{.ROOT_DIR}}"}}/build/appicon.png'
OUTPUTFILE: '-outputfile {{ "{{.ROOT_DIR}}"}}/build/appimage/{{ "{{.APP_NAME}}" }}.desktop'
## -------------------------- Misc -------------------------- ##