Compare commits

...

2 Commits

Author SHA1 Message Date
mixeme 2b636cbb65 P8.1: extract developer docs into docs/DEVELOPMENT.md
Move Requirements, Build, Run From Source, Project Layout, and
Dependencies/mirroring out of README into docs/DEVELOPMENT.md. Update
version references to 0.9.0 and correct the storage description to JSON.
README retains user-facing sections and gains a Development link.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-24 00:26:36 +03:00
mixeme 3907ab3c18 P7.1: move Windows-only runner tests into runner_windows_test.go
Tests touching SysProcAttr.HideWindow/CmdLine and windowsShellCommandLine
are guarded by //go:build windows; runner_test.go drops the winproc import.
Linux cross-compile and Windows test run both clean.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-24 00:23:25 +03:00
5 changed files with 244 additions and 253 deletions
+1 -193
View File
@@ -9,7 +9,7 @@ Project notes:
- [Changelog](docs/CHANGELOG.md)
- [Roadmap](docs/ROADMAP.md)
- [Architecture](docs/ARCHITECTURE.md)
- [Refactoring plan](docs/REFACTORING.md)
- [Development](docs/DEVELOPMENT.md)
## Features
@@ -24,158 +24,6 @@ Project notes:
- Windows tray support.
- Version shown in the window title, Settings, and build artifact names.
## Requirements
Common:
- [Go](https://go.dev/) 1.22 or newer.
Windows:
- MSYS2 with UCRT64 GCC in `C:\msys64\ucrt64\bin`.
Install these dependencies on Windows:
```powershell
# 1. Install Go 1.22 or newer from https://go.dev/dl/.
# The default installer path is C:\Program Files\Go.
go version
# 2. Install MSYS2 from https://www.msys2.org/.
# Use the default installation path so UCRT64 tools are placed under
# C:\msys64\ucrt64\bin.
# 3. Open "MSYS2 UCRT64" from the Start menu and install GCC plus windres.
pacman -Syu
pacman -S --needed mingw-w64-ucrt-x86_64-gcc mingw-w64-ucrt-x86_64-binutils
# 4. In PowerShell, check that the compiler is available where the build script
# expects it. build-windows.bat prepends this directory automatically.
Test-Path C:\msys64\ucrt64\bin\gcc.exe
Test-Path C:\msys64\ucrt64\bin\windres.exe
```
Linux:
- A C compiler.
- [Fyne](https://fyne.io/) native build dependencies, including OpenGL/X11 development packages.
On Debian/Ubuntu, the Linux dependencies are typically:
```bash
# Go builds the application, gcc is required by CGO/Fyne, and the OpenGL/X11
# development packages provide the native desktop headers used by Fyne.
sudo apt install golang gcc libgl1-mesa-dev xorg-dev
```
## Build
Windows:
```powershell
# Builds dist\windows\gosentry-<version>-windows-amd64.exe. The script changes
# to the repository root first, so double-clicking it from Explorer works. It
# also adds MSYS2 UCRT64 to PATH for this process only, embeds the Windows icon
# when windres is available, and uses the Windows GUI subsystem so no console
# window opens at startup.
.\scripts\build-windows.bat
```
The Windows build is created as a GUI application, so it does not open a terminal window.
The binary is written to:
```text
# GUI executable produced by scripts\build-windows.bat.
dist\windows\gosentry-0.3.0-windows-amd64.exe
```
Linux:
```bash
# Make the helper executable once, then build a linux/amd64 Fyne binary.
chmod +x ./scripts/build-linux.sh
./scripts/build-linux.sh
```
The binary is written to:
```text
# Linux executable produced by scripts/build-linux.sh.
dist/linux/gosentry-0.3.0-linux-amd64
```
Linux using Docker:
```bash
# Builds the Linux binary inside Docker using the versioned image tag
# gitea.mixdep.ru/mix/gosentry-builder:<version>. Useful from hosts or CI jobs
# where the native Linux/Fyne packages are not installed locally.
chmod +x ./scripts/build-linux-docker.sh
./scripts/build-linux-docker.sh
```
The binary is copied to:
```text
# Linux executable copied out of the Docker build image.
dist\linux\gosentry-0.3.0-linux-amd64
```
Release build from Linux:
```bash
# Interactively choose Linux amd64, Linux arm64, Windows amd64, or all artifacts
# from one Linux/Docker workflow. The Dockerfile contains the builder
# environment; the build commands live in this script. Docker runs the build
# with the current user's UID/GID so dist/ files are not owned by root.
chmod +x ./scripts/build-release-linux.sh
./scripts/build-release-linux.sh
```
Non-interactive release builds can pass target names:
```bash
# Build only Linux arm64 and Windows amd64 artifacts.
./scripts/build-release-linux.sh linux-arm64 windows-amd64
```
The binaries are copied to:
```text
# Linux artifact.
dist/linux/gosentry-0.3.0-linux-amd64
# Linux arm64 artifact.
dist/linux/gosentry-0.3.0-linux-arm64
# Windows artifact cross-compiled from Linux.
dist/windows/gosentry-0.3.0-windows-amd64.exe
```
## Run From Source
Windows:
```powershell
# Fyne requires CGO on Windows. MSYS2 UCRT64 provides the C compiler and native
# libraries used by the desktop backend.
$env:Path = 'C:\msys64\ucrt64\bin;' + $env:Path
$env:CGO_ENABLED = '1'
# go run starts the app from source. Use scripts\build-windows.bat when you need
# a standalone .exe without a console window.
& 'C:\Program Files\Go\bin\go.exe' run ./cmd/gosentry
```
Linux:
```bash
# CGO must stay enabled because the Fyne GUI links against native Linux desktop
# libraries.
CGO_ENABLED=1 go run ./cmd/gosentry
```
## Troubleshooting
### Windows, VirtualBox, RDP, And OpenGL
@@ -350,43 +198,3 @@ Windows:
%APPDATA%\Microsoft\Windows\Start Menu\Programs\Startup\GoSentry.lnk
```
## Project Layout
- `cmd/gosentry` — entry point; starts the desktop app.
- `src/domain` — pure value types: `Job`, `Config`, `RunRecord`, `Schedule`, `JobRuntime`.
- `src/app``Service`: sole owner of job and runtime state; emits typed events to the UI.
- `src/scheduler` — pure timing loop; calls `Service.RunDue` on every tick.
- `src/runner` — shell command execution, log file writing, and log cleanup.
- `src/storage` — YAML persistence (`gosentry.yaml`, `jobs.yaml`).
- `src/platform/autostart``Manager` interface with Windows (shortcut) and Linux (XDG) implementations.
- `src/platform/desktop` — display-scale helper (Linux only).
- `src/platform/winproc` — hidden-window startup flags (Windows only).
- `src/ui` — Fyne windows, tabs, and dialogs; reads service state through events.
- `assets` — app icons embedded into the application binary.
- `scripts` — build helpers.
- `docs` — architecture notes, changelog, and roadmap.
Build outputs are written to `dist/`.
## Dependencies
GoSentry keeps the direct dependency list intentionally small:
- [`fyne.io/fyne/v2`](https://fyne.io/) for the native GUI.
- `github.com/robfig/cron/v3` for cron schedule parsing.
- [`go.yaml.in/yaml/v4`](https://github.com/yaml/go-yaml) for YAML settings and jobs.
The remaining entries in `go.mod` are indirect dependencies pulled by Fyne and the Go module resolver.
Source repositories for mirroring:
- Go toolchain: https://go.googlesource.com/go
- Fyne: https://github.com/fyne-io/fyne
- robfig/cron: https://github.com/robfig/cron
- yaml/go-yaml: https://github.com/yaml/go-yaml
To list every direct and indirect Go module used by the current checkout:
```bash
go list -m all
```
+188
View File
@@ -0,0 +1,188 @@
# GoSentry — Development
Build instructions, project layout, and dependency information for contributors.
## Requirements
Common:
- [Go](https://go.dev/) 1.22 or newer.
Windows:
- MSYS2 with UCRT64 GCC in `C:\msys64\ucrt64\bin`.
Install these dependencies on Windows:
```powershell
# 1. Install Go 1.22 or newer from https://go.dev/dl/.
# The default installer path is C:\Program Files\Go.
go version
# 2. Install MSYS2 from https://www.msys2.org/.
# Use the default installation path so UCRT64 tools are placed under
# C:\msys64\ucrt64\bin.
# 3. Open "MSYS2 UCRT64" from the Start menu and install GCC plus windres.
pacman -Syu
pacman -S --needed mingw-w64-ucrt-x86_64-gcc mingw-w64-ucrt-x86_64-binutils
# 4. In PowerShell, check that the compiler is available where the build script
# expects it. build-windows.bat prepends this directory automatically.
Test-Path C:\msys64\ucrt64\bin\gcc.exe
Test-Path C:\msys64\ucrt64\bin\windres.exe
```
Linux:
- A C compiler.
- [Fyne](https://fyne.io/) native build dependencies, including OpenGL/X11 development packages.
On Debian/Ubuntu, the Linux dependencies are typically:
```bash
# Go builds the application, gcc is required by CGO/Fyne, and the OpenGL/X11
# development packages provide the native desktop headers used by Fyne.
sudo apt install golang gcc libgl1-mesa-dev xorg-dev
```
## Build
Windows:
```powershell
# Builds dist\windows\gosentry-<version>-windows-amd64.exe. The script changes
# to the repository root first, so double-clicking it from Explorer works. It
# also adds MSYS2 UCRT64 to PATH for this process only, embeds the Windows icon
# when windres is available, and uses the Windows GUI subsystem so no console
# window opens at startup.
.\scripts\build-windows.bat
```
The Windows build is created as a GUI application, so it does not open a terminal window.
The binary is written to:
```text
dist\windows\gosentry-0.9.0-windows-amd64.exe
```
Linux:
```bash
# Make the helper executable once, then build a linux/amd64 Fyne binary.
chmod +x ./scripts/build-linux.sh
./scripts/build-linux.sh
```
The binary is written to:
```text
dist/linux/gosentry-0.9.0-linux-amd64
```
Linux using Docker:
```bash
# Builds the Linux binary inside Docker using the versioned image tag
# gitea.mixdep.ru/mix/gosentry-builder:<version>. Useful from hosts or CI jobs
# where the native Linux/Fyne packages are not installed locally.
chmod +x ./scripts/build-linux-docker.sh
./scripts/build-linux-docker.sh
```
The binary is copied to:
```text
dist/linux/gosentry-0.9.0-linux-amd64
```
Release build from Linux:
```bash
# Interactively choose Linux amd64, Linux arm64, Windows amd64, or all artifacts
# from one Linux/Docker workflow. The Dockerfile contains the builder
# environment; the build commands live in this script. Docker runs the build
# with the current user's UID/GID so dist/ files are not owned by root.
chmod +x ./scripts/build-release-linux.sh
./scripts/build-release-linux.sh
```
Non-interactive release builds can pass target names:
```bash
# Build only Linux arm64 and Windows amd64 artifacts.
./scripts/build-release-linux.sh linux-arm64 windows-amd64
```
The binaries are copied to:
```text
dist/linux/gosentry-0.9.0-linux-amd64
dist/linux/gosentry-0.9.0-linux-arm64
dist/windows/gosentry-0.9.0-windows-amd64.exe
```
## Run From Source
Windows:
```powershell
# Fyne requires CGO on Windows. MSYS2 UCRT64 provides the C compiler and native
# libraries used by the desktop backend.
$env:Path = 'C:\msys64\ucrt64\bin;' + $env:Path
$env:CGO_ENABLED = '1'
# go run starts the app from source. Use scripts\build-windows.bat when you need
# a standalone .exe without a console window.
& 'C:\Program Files\Go\bin\go.exe' run ./cmd/gosentry
```
Linux:
```bash
# CGO must stay enabled because the Fyne GUI links against native Linux desktop
# libraries.
CGO_ENABLED=1 go run ./cmd/gosentry
```
## Project Layout
- `cmd/gosentry` — entry point; starts the desktop app.
- `src/domain` — pure value types: `Job`, `Config`, `RunRecord`, `Schedule`, `JobRuntime`.
- `src/app``Service`: sole owner of job and runtime state; emits typed events to the UI.
- `src/scheduler` — pure timing loop; calls `Service.RunDue` on every tick.
- `src/runner` — shell command execution, log file writing, and log cleanup.
- `src/storage` — JSON persistence (`gosentry.json`, `jobs.json`); one-time import from legacy YAML on first run.
- `src/platform/autostart``Manager` interface with Windows (shortcut) and Linux (XDG) implementations.
- `src/platform/desktop` — display-scale helper (Linux only).
- `src/platform/winproc` — hidden-window startup flags (Windows only).
- `src/ui` — Fyne windows, tabs, and dialogs; reads service state through events.
- `assets` — app icons embedded into the application binary.
- `scripts` — build helpers.
- `docs` — architecture notes, changelog, and roadmap.
Build outputs are written to `dist/`.
## Dependencies
GoSentry keeps the direct dependency list intentionally small:
- [`fyne.io/fyne/v2`](https://fyne.io/) for the native GUI.
- `github.com/robfig/cron/v3` for cron schedule parsing.
- [`go.yaml.in/yaml/v4`](https://github.com/yaml/go-yaml) for one-time import of legacy YAML config files.
The remaining entries in `go.mod` are indirect dependencies pulled by Fyne and the Go module resolver.
Source repositories for mirroring:
- Go toolchain: https://go.googlesource.com/go
- Fyne: https://github.com/fyne-io/fyne
- robfig/cron: https://github.com/robfig/cron
- yaml/go-yaml: https://github.com/yaml/go-yaml
To list every direct and indirect Go module used by the current checkout:
```bash
go list -m all
```
+2 -2
View File
@@ -114,10 +114,10 @@ These land together because both edit `domain/job.go` and `storage/store.go`.
- [x] P6.3 — Re-measure startup → PERFORMANCE.md
### Phase 7 — Roadmap follow-ups
- [ ] P7.1 — Linux test build fix (build-tagged Windows tests)
- [x] P7.1 — Linux test build fix (build-tagged Windows tests)
### Phase 8 — Docs + version
- [ ] P8.1 — `docs/DEVELOPMENT.md`
- [x] P8.1 — `docs/DEVELOPMENT.md`
- [ ] P8.2 — End-user README rewrite
- [ ] P8.3 — CHANGELOG + ROADMAP updates
-58
View File
@@ -10,7 +10,6 @@ import (
"time"
"gitea.mixdep.ru/mix/gosentry/src/domain"
"gitea.mixdep.ru/mix/gosentry/src/platform/winproc"
)
func echoCommand(message string) string {
@@ -322,60 +321,3 @@ func TestRunJobStartOnlyReportsStartFailure(t *testing.T) {
}
}
func TestDirectCommandDoesNotHideWindow(t *testing.T) {
if runtime.GOOS != "windows" {
t.Skip("Windows window visibility only")
}
invocation := jobInvocation(context.Background(), domain.Job{
Command: `C:\Windows\System32\cmd.exe`,
Arguments: "/C\necho visible direct process",
})
if invocation.hideWindow {
t.Fatal("direct command should not request hidden startup window")
}
}
func TestShellCommandHidesWindow(t *testing.T) {
if runtime.GOOS != "windows" {
t.Skip("Windows window visibility only")
}
invocation := jobInvocation(context.Background(), domain.Job{Command: "echo hidden shell process"})
if !invocation.hideWindow {
t.Fatal("shell command should request hidden startup window")
}
winproc.ConfigureHiddenWindow(invocation.command)
if invocation.command.SysProcAttr == nil || !invocation.command.SysProcAttr.HideWindow {
t.Fatal("expected shell command to be hidden")
}
}
func TestShellCommandUsesWindowsSafeQuoting(t *testing.T) {
if runtime.GOOS != "windows" {
t.Skip("Windows cmd.exe quoting only")
}
command := shellCommand(context.Background(), `"C:\Program Files\FreeFileSync\FreeFileSync.exe" "D:\Local\Programs\FreeFileSync\Jobs\Auto.ffs_batch"`)
winproc.ConfigureHiddenWindow(command)
want := `cmd.exe /S /C ""C:\Program Files\FreeFileSync\FreeFileSync.exe" "D:\Local\Programs\FreeFileSync\Jobs\Auto.ffs_batch""`
if command.SysProcAttr == nil {
t.Fatal("expected SysProcAttr")
}
if command.SysProcAttr.CmdLine != want {
t.Fatalf("expected command line %q, got %q", want, command.SysProcAttr.CmdLine)
}
}
func TestWindowsShellCommandLineQuotesUnquotedProgramPath(t *testing.T) {
if runtime.GOOS != "windows" {
t.Skip("Windows cmd.exe quoting only")
}
got := windowsShellCommandLine(`C:\Program Files\Joplin\Joplin.exe --profile "D:\Joplin Profile"`)
want := `cmd.exe /S /C ""C:\Program Files\Joplin\Joplin.exe" --profile "D:\Joplin Profile""`
if got != want {
t.Fatalf("expected command line %q, got %q", want, got)
}
}
+53
View File
@@ -0,0 +1,53 @@
//go:build windows
package runner
import (
"context"
"testing"
"gitea.mixdep.ru/mix/gosentry/src/domain"
"gitea.mixdep.ru/mix/gosentry/src/platform/winproc"
)
func TestDirectCommandDoesNotHideWindow(t *testing.T) {
invocation := jobInvocation(context.Background(), domain.Job{
Command: `C:\Windows\System32\cmd.exe`,
Arguments: "/C\necho visible direct process",
})
if invocation.hideWindow {
t.Fatal("direct command should not request hidden startup window")
}
}
func TestShellCommandHidesWindow(t *testing.T) {
invocation := jobInvocation(context.Background(), domain.Job{Command: "echo hidden shell process"})
if !invocation.hideWindow {
t.Fatal("shell command should request hidden startup window")
}
winproc.ConfigureHiddenWindow(invocation.command)
if invocation.command.SysProcAttr == nil || !invocation.command.SysProcAttr.HideWindow {
t.Fatal("expected shell command to be hidden")
}
}
func TestShellCommandUsesWindowsSafeQuoting(t *testing.T) {
command := shellCommand(context.Background(), `"C:\Program Files\FreeFileSync\FreeFileSync.exe" "D:\Local\Programs\FreeFileSync\Jobs\Auto.ffs_batch"`)
winproc.ConfigureHiddenWindow(command)
want := `cmd.exe /S /C ""C:\Program Files\FreeFileSync\FreeFileSync.exe" "D:\Local\Programs\FreeFileSync\Jobs\Auto.ffs_batch""`
if command.SysProcAttr == nil {
t.Fatal("expected SysProcAttr")
}
if command.SysProcAttr.CmdLine != want {
t.Fatalf("expected command line %q, got %q", want, command.SysProcAttr.CmdLine)
}
}
func TestWindowsShellCommandLineQuotesUnquotedProgramPath(t *testing.T) {
got := windowsShellCommandLine(`C:\Program Files\Joplin\Joplin.exe --profile "D:\Joplin Profile"`)
want := `cmd.exe /S /C ""C:\Program Files\Joplin\Joplin.exe" --profile "D:\Joplin Profile""`
if got != want {
t.Fatalf("expected command line %q, got %q", want, got)
}
}