Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
92 changes: 92 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
# Continuous Integration workflow for hunks.
# Note: Byte-for-byte vasmm68k_mot reassembly parity on real Amiga targets is a
# local development verification step (see TESTING.md); CI runs strictly on
# synthetic, copyright-free unit tests (AGENTS.md Invariant #6).
name: CI

on:
push:
branches: ['**']
tags-ignore: ['v*']
pull_request:
branches: [master]

permissions:
contents: read

concurrency:
group: ci-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

env:
CGO_ENABLED: "0"

jobs:
lint:
name: Lint & Invariants
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Setup Go
uses: actions/setup-go@v5
with:
go-version-file: go.mod
check-latest: true

- name: Check gofmt
run: |
unformatted=$(gofmt -l .)
if [ -n "$unformatted" ]; then
echo "The following files are not formatted with gofmt:"
echo "$unformatted"
gofmt -d .
exit 1
fi

- name: Verify Go modules
run: |
go mod verify
go mod tidy
git diff --exit-code go.mod go.sum

- name: Run go vet
run: go vet ./...

- name: Verify SQL isolation invariant (AGENTS.md Invariant #2)
run: |
if grep -rnE '\.(Query|QueryRow|Exec)\(' cmd/ internal/analyzer/ internal/emitter/ internal/callgraph/ internal/project/ internal/tui/ --exclude='*_test.go'; then
echo "Error: raw SQL found outside internal/db/ (violates AGENTS.md Hard Invariant #2)"
exit 1
fi

test:
name: Build & Test (${{ matrix.os }})
needs: lint
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
runs-on: ${{ matrix.os }}
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Setup Go
uses: actions/setup-go@v5
with:
go-version-file: go.mod
check-latest: true

- name: Build CLI (pure Go, CGO_ENABLED=0)
run: go build -v -trimpath ./cmd/hunks

- name: Run unit tests (CGO_ENABLED=0)
run: go test -v ./...

- name: Run race detector (Linux)
if: runner.os == 'Linux'
env:
CGO_ENABLED: "1"
run: go test -race ./...
99 changes: 99 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
name: Release

on:
push:
tags:
- 'v[0-9]+.[0-9]+.[0-9]*'

permissions:
contents: write

env:
CGO_ENABLED: "0"

jobs:
release:
name: Build & Publish Release
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0

- name: Verify tag is on master
run: |
git fetch origin master
if ! git merge-base --is-ancestor HEAD origin/master; then
echo "Error: tag ${GITHUB_REF_NAME} ($(git rev-parse --short HEAD)) is not an ancestor of origin/master"
exit 1
fi

- name: Setup Go
uses: actions/setup-go@v5
with:
go-version-file: go.mod
check-latest: true

- name: Pre-release verification gate (gofmt, go vet, go test)
run: |
test -z "$(gofmt -l .)"
go vet ./...
go test -v ./...

- name: Cross-compile and package release archives
run: |
set -euo pipefail
VERSION="${GITHUB_REF_NAME}"
mkdir -p dist

targets=(
"linux:amd64"
"linux:arm64"
"darwin:amd64"
"darwin:arm64"
"windows:amd64"
"windows:arm64"
)

for target in "${targets[@]}"; do
GOOS="${target%%:*}"
GOARCH="${target##*:}"
STAGE="stage/hunks_${VERSION}_${GOOS}_${GOARCH}"
mkdir -p "${STAGE}"

BIN="hunks"
if [ "${GOOS}" = "windows" ]; then
BIN="hunks.exe"
fi

echo "Building ${GOOS}/${GOARCH} -> ${STAGE}/${BIN}"
GOOS="${GOOS}" GOARCH="${GOARCH}" CGO_ENABLED=0 \
go build -trimpath -ldflags="-s -w -X main.version=${VERSION}" \
-o "${STAGE}/${BIN}" ./cmd/hunks

cp README.md MANUAL.md "${STAGE}/"

if [ "${GOOS}" = "windows" ]; then
(cd "${STAGE}" && zip -q "../../dist/hunks_${VERSION}_${GOOS}_${GOARCH}.zip" "${BIN}" README.md MANUAL.md)
else
tar -C "${STAGE}" -czf "dist/hunks_${VERSION}_${GOOS}_${GOARCH}.tar.gz" "${BIN}" README.md MANUAL.md
fi
done

(cd dist && sha256sum hunks_* > checksums.txt)
ls -lh dist/
cat dist/checksums.txt

- name: Publish GitHub Release
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
if gh release view "${GITHUB_REF_NAME}" >/dev/null 2>&1; then
gh release upload "${GITHUB_REF_NAME}" dist/* --clobber
else
gh release create "${GITHUB_REF_NAME}" dist/* \
--title "${GITHUB_REF_NAME}" \
--generate-notes \
--verify-tag
fi
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ out.s
*.rom
*.kick
*.mod
!go.mod
*.xm
*.lha
*.lzx
Expand Down
3 changes: 3 additions & 0 deletions cmd/hunks/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ import (
"github.com/spf13/cobra"
)

var version = "dev"

func parseHex(val string) (uint32, error) {
return project.ParseUint32(val)
}
Expand Down Expand Up @@ -271,6 +273,7 @@ func main() {
rootCmd := &cobra.Command{
Use: "hunks",
Short: "hunks — Amiga M68k Reverse Engineering System",
Version: version,
SilenceUsage: true,
}
rootCmd.PersistentFlags().StringVarP(&workspacePath, "path", "p", ".", "Workspace root directory (defaults to current working directory)")
Expand Down
42 changes: 42 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
module github.com/mikolas/hunks

go 1.27

require (
github.com/charmbracelet/bubbletea v1.3.10
github.com/charmbracelet/x/term v0.2.1
github.com/google/btree v1.1.3
github.com/jenska/m68kemu v1.8.1
github.com/mattn/go-runewidth v0.0.16
github.com/muesli/cancelreader v0.2.2
github.com/spf13/cobra v1.10.2
modernc.org/sqlite v1.61.0
)

require (
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect
github.com/charmbracelet/lipgloss v1.1.0 // indirect
github.com/charmbracelet/x/ansi v0.10.1 // indirect
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd // indirect
github.com/dustin/go-humanize v1.1.0 // indirect
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/jenska/m68kdasm v1.4.1 // indirect
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
github.com/mattn/go-isatty v0.0.24 // indirect
github.com/mattn/go-localereader v0.0.1 // indirect
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
github.com/muesli/termenv v0.16.0 // indirect
github.com/ncruces/go-strftime v1.1.0 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/spf13/pflag v1.0.9 // indirect
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
golang.org/x/sys v0.49.0 // indirect
golang.org/x/text v0.3.8 // indirect
modernc.org/libc v1.79.0 // indirect
modernc.org/mathutil v1.7.1 // indirect
modernc.org/memory v1.12.1 // indirect
)
4 changes: 2 additions & 2 deletions internal/analyzer/chipset.go
Original file line number Diff line number Diff line change
Expand Up @@ -109,10 +109,10 @@ const (
)

const (
copperMaxSteps = 1024
copperMaxSteps = 1024
HPosPerLine uint32 = 227
VPosPerFrame uint32 = 313
CyclesPerLine uint32 = HPosPerLine * 2 // 454 CPU cycles
CyclesPerLine uint32 = HPosPerLine * 2 // 454 CPU cycles
CyclesPerFrame uint32 = VPosPerFrame * CyclesPerLine // 142,102 CPU cycles

dsksyncPeriodCycles uint32 = 12800
Expand Down
14 changes: 7 additions & 7 deletions internal/emitter/parity_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,13 @@ func buildSyntheticHunkExecutable() []byte {

// HUNK_HEADER: 3 hunks (CODE public 28B, DATA chip 8B on disk / 16B alloc, BSS chip 32B)
push(hunk.HunkHeader)
push(0) // resident libs
push(3) // table_size = 3
push(0) // first_hunk = 0
push(2) // last_hunk = 2
push(7) // hunk0: 7 longs (28B) CODE public
push((1 << 30) | 4) // hunk1: 4 longs (16B alloc) DATA chip
push((1 << 30) | 8) // hunk2: 8 longs (32B alloc) BSS chip
push(0) // resident libs
push(3) // table_size = 3
push(0) // first_hunk = 0
push(2) // last_hunk = 2
push(7) // hunk0: 7 longs (28B) CODE public
push((1 << 30) | 4) // hunk1: 4 longs (16B alloc) DATA chip
push((1 << 30) | 8) // hunk2: 8 longs (32B alloc) BSS chip

// Hunk 0: CODE (28 bytes = 7 longs)
// +0x00: 48 E7 40 20 movem.l d1/a2,-(a7)
Expand Down
14 changes: 7 additions & 7 deletions internal/hunk/parser_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,13 @@ func TestParseSyntheticMultiHunkBinary(t *testing.T) {

// HUNK_HEADER: 3 hunks (CODE public 8B, DATA chip 8B, BSS chip 64B)
push(HunkHeader)
push(0) // resident libs
push(3) // table_size = 3
push(0) // first_hunk = 0
push(2) // last_hunk = 2
push(2) // hunk0: 2 longs (8B) public
push((1 << 30) | 2) // hunk1: 2 longs (8B) chip
push((1 << 30) | 16) // hunk2: 16 longs (64B) chip
push(0) // resident libs
push(3) // table_size = 3
push(0) // first_hunk = 0
push(2) // last_hunk = 2
push(2) // hunk0: 2 longs (8B) public
push((1 << 30) | 2) // hunk1: 2 longs (8B) chip
push((1 << 30) | 16) // hunk2: 16 longs (64B) chip

// Hunk 0: CODE + RELOC32 to hunk 1
push(HunkCode)
Expand Down
1 change: 0 additions & 1 deletion internal/m68k/cycles_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -487,4 +487,3 @@ func TestExhaustiveAll65536OpcodesHaveNonZeroTiming(t *testing.T) {
t.Fatalf("expected >40000 valid M68000 opcodes, got %d", validCount)
}
}

5 changes: 3 additions & 2 deletions internal/modules/modules.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,8 +72,9 @@ type Recognizer interface {
}

// Recognizers returns the built-in recognizer registry in ExoticRipper format-ID order:
// 2 Final Music System, 5/6 Future Composer, 22 SoundTracker 2.6, 23 ProTracker,
// 35 David Whittaker, 42 Old SoundTracker, 43 Delta Music 1.0.
//
// 2 Final Music System, 5/6 Future Composer, 22 SoundTracker 2.6, 23 ProTracker,
// 35 David Whittaker, 42 Old SoundTracker, 43 Delta Music 1.0.
func Recognizers() []Recognizer {
return []Recognizer{
FinalMusicSystem{},
Expand Down
22 changes: 11 additions & 11 deletions internal/project/project.go
Original file line number Diff line number Diff line change
Expand Up @@ -345,17 +345,17 @@ type AnalysisData struct {
}

type ProjectData struct {
TargetName string `json:"target_name"`
Segments []Segment `json:"segments"`
Spaces []Space `json:"spaces"`
ActiveSpace string `json:"active_space"`
Output string `json:"output"`
EntryPoints []EntryPoint `json:"entry_points"`
MemoryRegions []MemoryRegion `json:"memory_regions"`
Structs map[string]map[string]string `json:"structs"`
RegisterAliases map[string]string `json:"register_aliases"`
Carves []Carve `json:"carves"`
Analysis AnalysisData `json:"_analysis"`
TargetName string `json:"target_name"`
Segments []Segment `json:"segments"`
Spaces []Space `json:"spaces"`
ActiveSpace string `json:"active_space"`
Output string `json:"output"`
EntryPoints []EntryPoint `json:"entry_points"`
MemoryRegions []MemoryRegion `json:"memory_regions"`
Structs map[string]map[string]string `json:"structs"`
RegisterAliases map[string]string `json:"register_aliases"`
Carves []Carve `json:"carves"`
Analysis AnalysisData `json:"_analysis"`
}

type Project struct {
Expand Down
1 change: 0 additions & 1 deletion internal/tui/actions.go
Original file line number Diff line number Diff line change
Expand Up @@ -479,4 +479,3 @@ func (a *App) CarveBitplaneRange() {
a.ReloadAtCursor()
a.StatusMsg = fmt.Sprintf("Carved bitplane range +$%05X..+$%05X (%s)", startOff, endOff, label)
}

Loading
Loading