diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..d5d1f57 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,7 @@ +.git +bin +dist +logs +pysentry.yaml +jobs.yaml +*.exe diff --git a/.gitignore b/.gitignore index 8324761..26cb697 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,12 @@ # ---> Python # ---> Go bin/ +dist/ +cmd/pysentry/*.syso *.exe +pysentry.yaml +jobs.yaml +logs/ # Byte-compiled / optimized / DLL files __pycache__/ diff --git a/Dockerfile.linux b/Dockerfile.linux new file mode 100644 index 0000000..96db289 --- /dev/null +++ b/Dockerfile.linux @@ -0,0 +1,22 @@ +FROM golang:1.22-bookworm + +RUN apt-get update && \ + apt-get install -y --no-install-recommends \ + ca-certificates \ + gcc \ + libgl1-mesa-dev \ + xorg-dev && \ + rm -rf /var/lib/apt/lists/* + +WORKDIR /src + +COPY go.mod go.sum ./ +RUN go mod download + +COPY . . + +ENV CGO_ENABLED=1 +ENV GOOS=linux +ENV GOARCH=amd64 + +RUN go build -trimpath -ldflags "-s -w" -o /out/pysentry ./cmd/pysentry diff --git a/README.md b/README.md index 5d6d4c5..0eeeb86 100644 --- a/README.md +++ b/README.md @@ -1,37 +1,173 @@ # PySentry -PySentry is a cross-platform desktop scheduler inspired by cron. +PySentry is a cross-platform desktop scheduler inspired by cron. It provides a native GUI for creating, grouping, pausing, running, and monitoring scheduled shell commands. -The project is starting with the GUI shell first, then the scheduling core. +## Features + +- Native desktop GUI built with Fyne. +- Job storage in one clean YAML file. +- App settings in a separate YAML file. +- `@every` schedules and standard 5-field cron expressions. +- Manual and scheduled command runs. +- Per-run `.log` files with stdout/stderr. +- Log cleanup by maximum file count and maximum age. +- Global pause/resume for all job execution. +- Windows tray support. ## Requirements -- Go 1.22 or newer -- A C compiler for Fyne builds on Windows, for example MSYS2/MinGW-w64 +Common: -## Run +- Go 1.22 or newer. -```powershell -go mod tidy -go run ./cmd/pysentry +Windows: + +- MSYS2 with UCRT64 GCC in `C:\msys64\ucrt64\bin`. + +Linux: + +- A C compiler. +- Fyne native build dependencies, including OpenGL/X11 development packages. + +On Debian/Ubuntu, the Linux dependencies are typically: + +```bash +sudo apt install golang gcc libgl1-mesa-dev xorg-dev ``` -If Go is installed but not available in `PATH`, use the full path: +## Build + +Windows: ```powershell +.\scripts\build-windows.ps1 +``` + +The binary is written to: + +```text +dist\windows\pysentry.exe +``` + +Linux: + +```bash +chmod +x ./scripts/build-linux.sh +./scripts/build-linux.sh +``` + +The binary is written to: + +```text +dist/linux/pysentry +``` + +Linux using Docker: + +```powershell +.\scripts\build-linux-docker.ps1 +``` + +The binary is copied to: + +```text +dist\linux\pysentry +``` + +## Run From Source + +Windows: + +```powershell +$env:Path = 'C:\msys64\ucrt64\bin;' + $env:Path +$env:CGO_ENABLED = '1' & 'C:\Program Files\Go\bin\go.exe' run ./cmd/pysentry ``` -## Current shape +Linux: + +```bash +CGO_ENABLED=1 go run ./cmd/pysentry +``` + +## Storage + +PySentry creates its runtime files next to the executable by default. + +`pysentry.yaml` stores application settings: + +```yaml +jobs_dir: . +logs_dir: logs +max_log_files: 100 +max_log_age_days: 30 +keep_running_in_tray: true +notify_on_failure: true +``` + +`jobs.yaml` stores only job definitions: + +```yaml +jobs: + - id: 1 + name: Hello scheduler + folder: Examples + schedule: '@every 10s' + command: echo PySentry test job: scheduler is alive + enabled: true +``` + +Command output is written to separate files under `logs_dir`. File names include the run timestamp and job name, for example: + +```text +20260614-224306_Hello_scheduler.log +``` + +## Schedules + +Fast interval schedules: + +```text +@every 10s +@every 5m +@every 1h30m +``` + +Standard 5-field cron schedules: + +```text +*/5 * * * * every five minutes +0 2 * * * every day at 02:00 +30 9 * * 1-5 weekdays at 09:30 +``` + +## Using The App + +1. Start PySentry. +2. Use `New job` to create a command. +3. Set `Schedule`, `Command`, optional `Folder`, and `Enabled`. +4. Use `Run now` for a manual test run. +5. Use `Pause` to disable one job. +6. Use `Pause all` as a global stop switch. +7. Open `History` to see whether a run was `Manual`, `Schedule`, or `UI`. +8. Open `Settings` to change `jobs_dir`, `logs_dir`, and log cleanup limits. Use `Browse` to choose directories. + +Changing `jobs_dir` saves the current job list to the new directory. + +## Project Layout - `cmd/pysentry` starts the desktop app. -- `internal/app` contains the first Fyne-based interface prototype. -- `internal/core` contains YAML storage, command execution, and the first scheduler loop. -- Jobs can be created, edited, paused/resumed, run manually, and persisted to YAML. -- Settings are stored in `pysentry.yaml` next to the executable. -- Jobs are stored in one `jobs.yaml` file. The job directory is configured by `jobs_dir` and defaults to the executable directory. -- Command output is also written to per-run `.log` files in `logs_dir`. Log filenames include the run timestamp and job name. -- Log cleanup is controlled by `max_log_files` and `max_log_age_days`. -- The current scheduler supports `@every` schedules such as `@every 10s` and `@every 1m`. -- Run history records include a `trigger` value such as `Manual`, `Schedule`, or `UI`. -- Cron expression parsing is planned for the next phase. +- `internal/app` contains the GUI. +- `internal/core` contains YAML storage, command execution, scheduling, and log cleanup. +- `assets` contains app icons. +- `scripts` contains build helpers. + +## Dependencies + +PySentry keeps the direct dependency list intentionally small: + +- `fyne.io/fyne/v2` for the native GUI. +- `github.com/robfig/cron/v3` for cron schedule parsing. +- `gopkg.in/yaml.v3` for YAML settings and jobs. + +The remaining entries in `go.mod` are indirect dependencies pulled by Fyne and the Go module resolver. diff --git a/assets/pysentry-icon.png b/assets/pysentry-icon.png index 1ce55cb..bcc1ddd 100644 Binary files a/assets/pysentry-icon.png and b/assets/pysentry-icon.png differ diff --git a/go.mod b/go.mod index 9faa01f..968f746 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,7 @@ go 1.22 require ( fyne.io/fyne/v2 v2.5.3 + github.com/robfig/cron/v3 v3.0.1 gopkg.in/yaml.v3 v3.0.1 ) diff --git a/go.sum b/go.sum index 0956cc2..dc91322 100644 --- a/go.sum +++ b/go.sum @@ -243,6 +243,8 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= +github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= diff --git a/internal/app/app.go b/internal/app/app.go index d1ede49..abe8907 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -1,9 +1,8 @@ package app import ( + _ "embed" "fmt" - "os" - "path/filepath" "strconv" "strings" "time" @@ -27,6 +26,9 @@ const noFolder = "No folder" type job = core.Job type event = core.RunRecord +//go:embed assets/pysentry-icon.png +var iconBytes []byte + func Run() { a := app.NewWithID(appID) a.SetIcon(loadAppIcon()) @@ -39,19 +41,7 @@ func Run() { } func loadAppIcon() fyne.Resource { - candidates := []string{} - if executable, err := os.Executable(); err == nil { - candidates = append(candidates, filepath.Join(filepath.Dir(executable), "assets", "pysentry-icon.png")) - } - if workingDir, err := os.Getwd(); err == nil { - candidates = append(candidates, filepath.Join(workingDir, "assets", "pysentry-icon.png")) - } - for _, path := range candidates { - if resource, err := fyne.LoadResourceFromPath(path); err == nil { - return resource - } - } - return theme.ComputerIcon() + return fyne.NewStaticResource("pysentry-icon.png", iconBytes) } func configureSystemTray(a fyne.App, w fyne.Window) { @@ -379,7 +369,7 @@ func newMainView(w fyne.Window) fyne.CanvasObject { tabs := container.NewAppTabs( container.NewTabItemWithIcon("Jobs", theme.ListIcon(), container.NewHSplit(sidebar, container.NewPadded(details))), container.NewTabItemWithIcon("History", theme.HistoryIcon(), history), - container.NewTabItemWithIcon("Settings", theme.SettingsIcon(), settingsView(store)), + container.NewTabItemWithIcon("Settings", theme.SettingsIcon(), settingsView(w, store, &jobs)), ) tabs.SetTabLocation(container.TabLocationTop) @@ -557,14 +547,22 @@ func newHistoryView(events *[]event) *fyne.Container { return container.NewPadded(list) } -func settingsView(store *core.Store) fyne.CanvasObject { +func settingsView(w fyne.Window, store *core.Store, jobs *[]job) fyne.CanvasObject { runOnStartup := widget.NewCheck("Start PySentry when I sign in", nil) minimizeToTray := widget.NewCheck("Keep running in the system tray", nil) minimizeToTray.SetChecked(store.Config.KeepRunningInTray) notifications := widget.NewCheck("Show desktop notifications for failed jobs", nil) notifications.SetChecked(store.Config.NotifyOnFailure) + jobsDir := widget.NewEntry() + jobsDir.SetText(store.Config.JobsDir) + jobsDirBrowse := widget.NewButtonWithIcon("Browse", theme.FolderOpenIcon(), func() { + chooseFolder(w, jobsDir) + }) logsDir := widget.NewEntry() logsDir.SetText(store.Config.LogsDir) + logsDirBrowse := widget.NewButtonWithIcon("Browse", theme.FolderOpenIcon(), func() { + chooseFolder(w, logsDir) + }) maxLogFiles := widget.NewEntry() maxLogFiles.SetText(strconv.Itoa(store.Config.MaxLogFiles)) maxLogAgeDays := widget.NewEntry() @@ -583,6 +581,15 @@ func settingsView(store *core.Store) fyne.CanvasObject { return } store.Config.LogsDir = strings.TrimSpace(logsDir.Text) + if strings.TrimSpace(jobsDir.Text) == "" { + settingsStatus.SetText("Jobs directory is required") + return + } + if strings.TrimSpace(logsDir.Text) == "" { + settingsStatus.SetText("Logs directory is required") + return + } + store.Config.JobsDir = strings.TrimSpace(jobsDir.Text) store.Config.MaxLogFiles = files store.Config.MaxLogAgeDays = days store.Config.KeepRunningInTray = minimizeToTray.Checked @@ -591,6 +598,10 @@ func settingsView(store *core.Store) fyne.CanvasObject { settingsStatus.SetText("Save failed: " + err.Error()) return } + if err := store.SaveJobs(*jobs); err != nil { + settingsStatus.SetText("Jobs save failed: " + err.Error()) + return + } if err := core.CleanupLogs(store.Paths.LogsDir, store.Config.MaxLogFiles, store.Config.MaxLogAgeDays); err != nil { settingsStatus.SetText("Saved, cleanup failed: " + err.Error()) return @@ -606,9 +617,8 @@ func settingsView(store *core.Store) fyne.CanvasObject { widget.NewSeparator(), widget.NewLabelWithStyle("Storage", fyne.TextAlignLeading, fyne.TextStyle{Bold: true}), detailRow("Config YAML", widget.NewLabel(store.Paths.ConfigPath)), - detailRow("Jobs YAML", widget.NewLabel(store.Paths.JobsPath)), - detailRow("Jobs directory", widget.NewLabel(store.Paths.JobsDir)), - detailRow("Logs directory", logsDir), + detailRow("Jobs directory", container.NewBorder(nil, nil, nil, jobsDirBrowse, jobsDir)), + detailRow("Logs directory", container.NewBorder(nil, nil, nil, logsDirBrowse, logsDir)), detailRow("Max log files", maxLogFiles), detailRow("Max log age days", maxLogAgeDays), saveSettings, @@ -618,3 +628,12 @@ func settingsView(store *core.Store) fyne.CanvasObject { widget.NewLabel("Current core supports @every schedules. Cron expressions come next."), )) } + +func chooseFolder(w fyne.Window, target *widget.Entry) { + dialog.ShowFolderOpen(func(uri fyne.ListableURI, err error) { + if err != nil || uri == nil { + return + } + target.SetText(uri.Path()) + }, w) +} diff --git a/internal/app/assets/pysentry-icon.png b/internal/app/assets/pysentry-icon.png new file mode 100644 index 0000000..bcc1ddd Binary files /dev/null and b/internal/app/assets/pysentry-icon.png differ diff --git a/internal/core/model.go b/internal/core/model.go index e3eeb6f..4778ab3 100644 --- a/internal/core/model.go +++ b/internal/core/model.go @@ -22,11 +22,11 @@ type Job struct { Schedule string `yaml:"schedule"` Command string `yaml:"command"` Enabled bool `yaml:"enabled"` - LastRun string `yaml:"last_run,omitempty"` - NextRun string `yaml:"next_run,omitempty"` - LastState string `yaml:"last_state,omitempty"` - Logs []RunRecord `yaml:"activity,omitempty"` - Output string `yaml:"last_output,omitempty"` + LastRun string `yaml:"-"` + NextRun string `yaml:"-"` + LastState string `yaml:"-"` + Logs []RunRecord `yaml:"-"` + Output string `yaml:"-"` nextDue time.Time } diff --git a/internal/core/scheduler.go b/internal/core/scheduler.go index ea17692..5f59bc9 100644 --- a/internal/core/scheduler.go +++ b/internal/core/scheduler.go @@ -5,8 +5,12 @@ import ( "strings" "sync" "time" + + "github.com/robfig/cron/v3" ) +var cronParser = cron.NewParser(cron.Minute | cron.Hour | cron.Dom | cron.Month | cron.Dow | cron.Descriptor) + type Scheduler struct { store *Store jobs *[]Job @@ -146,24 +150,31 @@ func (s *Scheduler) resetNextRuns(now time.Time) { } func (s *Scheduler) prepareNextRun(job *Job, from time.Time) { - interval, ok := parseEvery(job.Schedule) + next, ok := nextRunTime(job.Schedule, from) if !ok { - job.NextRun = "Unsupported schedule" + job.NextRun = "Invalid schedule" job.nextDue = time.Time{} return } - job.nextDue = from.Add(interval) + job.nextDue = next job.NextRun = job.nextDue.Format("2006-01-02 15:04:05") } -func parseEvery(schedule string) (time.Duration, bool) { +func nextRunTime(schedule string, from time.Time) (time.Time, bool) { schedule = strings.TrimSpace(schedule) - if !strings.HasPrefix(schedule, "@every ") { - return 0, false + if schedule == "" { + return time.Time{}, false } - interval, err := time.ParseDuration(strings.TrimSpace(strings.TrimPrefix(schedule, "@every "))) - if err != nil || interval <= 0 { - return 0, false + if strings.HasPrefix(schedule, "@every ") { + interval, err := time.ParseDuration(strings.TrimSpace(strings.TrimPrefix(schedule, "@every "))) + if err != nil || interval <= 0 { + return time.Time{}, false + } + return from.Add(interval), true } - return interval, true + parsed, err := cronParser.Parse(schedule) + if err != nil { + return time.Time{}, false + } + return parsed.Next(from), true } diff --git a/internal/core/scheduler_test.go b/internal/core/scheduler_test.go new file mode 100644 index 0000000..066076f --- /dev/null +++ b/internal/core/scheduler_test.go @@ -0,0 +1,29 @@ +package core + +import ( + "testing" + "time" +) + +func TestNextRunTimeSupportsEvery(t *testing.T) { + from := time.Date(2026, 6, 14, 12, 0, 0, 0, time.UTC) + next, ok := nextRunTime("@every 10s", from) + if !ok { + t.Fatal("expected @every schedule to parse") + } + if want := from.Add(10 * time.Second); !next.Equal(want) { + t.Fatalf("expected %s, got %s", want, next) + } +} + +func TestNextRunTimeSupportsCron(t *testing.T) { + from := time.Date(2026, 6, 14, 12, 3, 0, 0, time.UTC) + next, ok := nextRunTime("*/5 * * * *", from) + if !ok { + t.Fatal("expected cron schedule to parse") + } + want := time.Date(2026, 6, 14, 12, 5, 0, 0, time.UTC) + if !next.Equal(want) { + t.Fatalf("expected %s, got %s", want, next) + } +} diff --git a/internal/core/store.go b/internal/core/store.go index 20c2ec1..064e1e8 100644 --- a/internal/core/store.go +++ b/internal/core/store.go @@ -36,6 +36,10 @@ func OpenStore() (*Store, []Job, error) { if err != nil { return nil, nil, err } + normalizeJobs(jobs) + if err := store.SaveJobs(jobs); err != nil { + return nil, nil, err + } return store, jobs, nil } @@ -93,6 +97,7 @@ func loadOrCreateConfig(paths Paths) (Config, error) { func loadOrCreateJobs(path string) ([]Job, error) { if _, err := os.Stat(path); errors.Is(err, os.ErrNotExist) { jobs := defaultJobs() + normalizeJobs(jobs) return jobs, writeYAML(path, JobsFile{Jobs: jobs}) } @@ -107,6 +112,42 @@ func loadOrCreateJobs(path string) ([]Job, error) { return file.Jobs, nil } +func normalizeJobs(jobs []Job) { + next := 1 + for index := range jobs { + job := &jobs[index] + if job.ID <= 0 { + job.ID = next + } + if job.ID >= next { + next = job.ID + 1 + } + if strings.TrimSpace(job.Name) == "" { + job.Name = "Untitled job" + } + if strings.TrimSpace(job.Schedule) == "" { + job.Schedule = "@every 1m" + } + if strings.TrimSpace(job.Command) == "" { + job.Command = echoCommand("PySentry job ran") + } + if job.LastRun == "" { + job.LastRun = "Never" + } + if job.Output == "" { + job.Output = "No command output captured yet." + } + if job.Enabled { + job.LastState = "Ready" + job.NextRun = "After start" + } else { + job.LastState = "Paused" + job.NextRun = "Paused" + } + job.Logs = nil + } +} + func resolveJobsDir(appDir string, jobsDir string) string { return resolveConfiguredDir(appDir, jobsDir) } @@ -138,39 +179,27 @@ func writeYAML(path string, value any) error { func defaultJobs() []Job { return []Job{ { - ID: 1, - Name: "Hello scheduler", - Folder: "Examples", - Schedule: "@every 10s", - Command: echoCommand("PySentry test job: scheduler is alive"), - Enabled: true, - LastRun: "Never", - NextRun: "After start", - LastState: "Ready", - Output: "No command output captured yet.", + ID: 1, + Name: "Hello scheduler", + Folder: "Examples", + Schedule: "@every 10s", + Command: echoCommand("PySentry test job: scheduler is alive"), + Enabled: true, }, { - ID: 2, - Name: "Write timestamp", - Folder: "Examples", - Schedule: "@every 15s", - Command: echoCommand("PySentry test job: timestamp command ran"), - Enabled: true, - LastRun: "Never", - NextRun: "After start", - LastState: "Ready", - Output: "No command output captured yet.", + ID: 2, + Name: "Write timestamp", + Folder: "Examples", + Schedule: "*/1 * * * *", + Command: echoCommand("PySentry test job: timestamp command ran"), + Enabled: true, }, { - ID: 3, - Name: "Paused sample", - Schedule: "@every 1m", - Command: echoCommand("This paused sample should not run until enabled"), - Enabled: false, - LastRun: "Never", - NextRun: "Paused", - LastState: "Paused", - Output: "No command output captured yet.", + ID: 3, + Name: "Paused sample", + Schedule: "@every 1m", + Command: echoCommand("This paused sample should not run until enabled"), + Enabled: false, }, } } diff --git a/internal/core/store_test.go b/internal/core/store_test.go new file mode 100644 index 0000000..2a44516 --- /dev/null +++ b/internal/core/store_test.go @@ -0,0 +1,38 @@ +package core + +import ( + "strings" + "testing" + + "gopkg.in/yaml.v3" +) + +func TestJobsYAMLDoesNotPersistRuntimeNoise(t *testing.T) { + jobs := []Job{ + { + ID: 1, + Name: "Clean job", + Schedule: "@every 10s", + Command: echoCommand("ok"), + Enabled: true, + LastRun: "2026-06-14 12:00:00", + NextRun: "2026-06-14 12:00:10", + LastState: "OK", + Output: "stdout: ok", + Logs: []RunRecord{ + {Time: "2026-06-14 12:00:00", JobName: "Clean job", Output: "stdout: ok"}, + }, + }, + } + + data, err := yaml.Marshal(JobsFile{Jobs: jobs}) + if err != nil { + t.Fatal(err) + } + text := string(data) + for _, unwanted := range []string{"last_run", "next_run", "last_state", "activity", "last_output", "stdout"} { + if strings.Contains(text, unwanted) { + t.Fatalf("jobs yaml should not contain %q:\n%s", unwanted, text) + } + } +} diff --git a/packaging/windows/pysentry.ico b/packaging/windows/pysentry.ico new file mode 100644 index 0000000..05f02e9 Binary files /dev/null and b/packaging/windows/pysentry.ico differ diff --git a/packaging/windows/pysentry.rc b/packaging/windows/pysentry.rc new file mode 100644 index 0000000..e65ae48 --- /dev/null +++ b/packaging/windows/pysentry.rc @@ -0,0 +1 @@ +IDI_ICON1 ICON "packaging/windows/pysentry.ico" diff --git a/scripts/build-linux-docker.ps1 b/scripts/build-linux-docker.ps1 new file mode 100644 index 0000000..b0bf96b --- /dev/null +++ b/scripts/build-linux-docker.ps1 @@ -0,0 +1,13 @@ +param( + [string]$Output = "dist\linux\pysentry" +) + +$ErrorActionPreference = "Stop" + +docker build -f Dockerfile.linux -t pysentry-linux-builder . +$containerId = docker create pysentry-linux-builder +New-Item -ItemType Directory -Force -Path (Split-Path $Output) | Out-Null +docker cp "${containerId}:/out/pysentry" $Output +docker rm $containerId | Out-Null + +Write-Host "Built $Output" diff --git a/scripts/build-linux.sh b/scripts/build-linux.sh new file mode 100644 index 0000000..b71a8df --- /dev/null +++ b/scripts/build-linux.sh @@ -0,0 +1,13 @@ +#!/usr/bin/env bash +set -euo pipefail + +output="${1:-dist/linux/pysentry}" +mkdir -p "$(dirname "$output")" + +export CGO_ENABLED=1 +export GOOS=linux +export GOARCH=amd64 + +go build -trimpath -ldflags "-s -w" -o "$output" ./cmd/pysentry + +echo "Built $output" diff --git a/scripts/build-windows.ps1 b/scripts/build-windows.ps1 new file mode 100644 index 0000000..013fe37 --- /dev/null +++ b/scripts/build-windows.ps1 @@ -0,0 +1,30 @@ +param( + [string]$Output = "dist\windows\pysentry.exe" +) + +$ErrorActionPreference = "Stop" + +$go = "${env:ProgramFiles}\Go\bin\go.exe" +if (-not (Test-Path $go)) { + $go = "go" +} + +$msys2Bin = "C:\msys64\ucrt64\bin" +if (Test-Path $msys2Bin) { + $env:Path = "$msys2Bin;$env:Path" +} + +$env:CGO_ENABLED = "1" +$env:GOOS = "windows" +$env:GOARCH = "amd64" + +New-Item -ItemType Directory -Force -Path (Split-Path $Output) | Out-Null + +$windres = Get-Command windres.exe -ErrorAction SilentlyContinue +if ($windres) { + & $windres.Source -O coff -o .\cmd\pysentry\rsrc_windows_amd64.syso .\packaging\windows\pysentry.rc +} + +& $go build -trimpath -ldflags "-s -w" -o $Output .\cmd\pysentry + +Write-Host "Built $Output"