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
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,17 @@ All notable changes are documented here, following
## [Unreleased]

### Added
- Build lifecycle and `anvil build`: runs deps, analyze, test, and an
unsigned/debug/simulator build for a detected project, with a live Bubble Tea
view and a plain non-TTY renderer. Flags `--path`, `--target`, `--flavor`,
`--release`, `--dry-run`, `--plain`.
- Driver contract (`internal/driver`) and drivers for Flutter, React Native,
native Android, native iOS, Swift (SPM), and Kotlin/JVM, with `--flavor`
threaded into build and test steps.
Comment on lines +14 to +16

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== File list ==\n'
git ls-files CHANGELOG.md docs/ROADMAP.md tasks/todo.md internal/driver | sed 's#^`#-` #'

printf '\n== Swift driver outline ==\n'
ast-grep outline internal/driver/swift.go --view expanded || true

printf '\n== Swift driver contents (relevant slice) ==\n'
nl -ba internal/driver/swift.go | sed -n '1,220p'

printf '\n== Roadmap slice ==\n'
nl -ba docs/ROADMAP.md | sed -n '1,120p'

printf '\n== Todo slice ==\n'
nl -ba tasks/todo.md | sed -n '1,120p'

printf '\n== Changelog slice ==\n'
nl -ba CHANGELOG.md | sed -n '1,80p'

printf '\n== Search for Flavor usage in driver code ==\n'
rg -n "Flavor|flavor" internal/driver docs tasks CHANGELOG.md

Repository: openforge-oss/anvil

Length of output: 774


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== Swift driver ==\n'
sed -n '1,220p' internal/driver/swift.go | cat -n

printf '\n== Roadmap ==\n'
sed -n '1,120p' docs/ROADMAP.md | cat -n

printf '\n== Todo ==\n'
sed -n '1,120p' tasks/todo.md | cat -n

printf '\n== Changelog ==\n'
sed -n '1,80p' CHANGELOG.md | cat -n

printf '\n== Flavor references in driver code ==\n'
rg -n "Flavor|flavor" internal/driver

Repository: openforge-oss/anvil

Length of output: 10782


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== Kotlin driver ==\n'
sed -n '1,220p' internal/driver/kotlin.go | cat -n

printf '\n== Flutter driver ==\n'
sed -n '1,220p' internal/driver/flutter.go | cat -n

printf '\n== Android driver ==\n'
sed -n '1,220p' internal/driver/android.go | cat -n

printf '\n== iOS driver ==\n'
sed -n '1,220p' internal/driver/ios.go | cat -n

Repository: openforge-oss/anvil

Length of output: 5888


Narrow the --flavor docs or add support in Swift/Kotlin/JVM. The changelog, roadmap, and todo entries overstate flavor coverage across all six stacks: internal/driver/swift.go and internal/driver/kotlin.go still ignore opts.Flavor. Either thread flavor through those drivers too or limit the docs to the stacks that actually support it.

📍 Affects 3 files
  • CHANGELOG.md#L14-L16 (this comment)
  • docs/ROADMAP.md#L10-L16
  • tasks/todo.md#L41-L44
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@CHANGELOG.md` around lines 14 - 16, Update CHANGELOG.md lines 14-16,
docs/ROADMAP.md lines 10-16, and tasks/todo.md lines 41-44 to limit --flavor
claims to the stacks that currently support it, unless internal/driver/swift.go
and internal/driver/kotlin.go are updated to propagate opts.Flavor through their
build and test steps. Keep the documentation consistent across all three files.

- Runner (`internal/pipeline`): executes steps, streams combined output, applies
the driver's classification, collects artifacts, and stops on first failure.
- Detection refinement: Gradle is not always Android and `Package.swift` is
Swift, adding Swift and Kotlin stacks.
- Stack detection engine (`internal/detect`): marker-file detectors for Flutter
(vs pure Dart; app/module/plugin subtypes), React Native (bare, Expo managed,
Expo prebuild), native Android (app vs library, KMP flag), and native iOS
Expand Down
136 changes: 136 additions & 0 deletions cmd/build.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
package cmd

import (
"context"
"errors"
"fmt"
"os"
"os/signal"
"path/filepath"
"strings"

"github.com/spf13/cobra"
"golang.org/x/term"

"github.com/openforge-oss/anvil/internal/detect"
"github.com/openforge-oss/anvil/internal/driver"
"github.com/openforge-oss/anvil/internal/pipeline"
"github.com/openforge-oss/anvil/internal/tui"
)

var (
buildPath string
buildTarget string
buildFlavor string
buildRelease bool
buildDryRun bool
buildPlain bool
)

var buildCmd = &cobra.Command{
Use: "build",
Short: "Run a detected project through deps, analyze, test, and build",
Args: cobra.NoArgs,
RunE: runBuild,
}

func init() {
buildCmd.Flags().StringVar(&buildPath, "path", ".", "project directory")
buildCmd.Flags().StringVar(&buildTarget, "target", "", "build target (stack-specific, e.g. apk, appbundle, ios)")
buildCmd.Flags().StringVar(&buildFlavor, "flavor", "", "build flavor, product flavor, or scheme")
buildCmd.Flags().BoolVar(&buildRelease, "release", false, "release build where applicable")
buildCmd.Flags().BoolVar(&buildDryRun, "dry-run", false, "print the steps without running them")
buildCmd.Flags().BoolVar(&buildPlain, "plain", false, "plain line output instead of the interactive view")
rootCmd.AddCommand(buildCmd)
}

func runBuild(cmd *cobra.Command, _ []string) error {
root, err := filepath.Abs(buildPath)
if err != nil {
return err
}

projects, err := detect.Scan(root, detect.DefaultOptions())
if err != nil {
return err
}
chosen, err := chooseProject(cmd, projects, root)
if err != nil {
return err
}

d, ok := driver.For(chosen.Stack, chosen.Path)
if !ok {
return fmt.Errorf("anvil build does not support the %q stack yet", chosen.Stack)
}

opts := driver.BuildOptions{Target: buildTarget, Flavor: buildFlavor, Release: buildRelease}

if buildDryRun {
printPlan(cmd, chosen, d, opts)
return nil
}

ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt)
defer stop()

events := make(chan pipeline.Event)
go pipeline.Run(ctx, chosen.Path, d, opts, events)

var success bool
if usePlain() {
success = tui.Plain(events, cmd.OutOrStdout())
} else {
success, err = tui.Run(events)
if err != nil {
return err
}
}
if !success {
return errors.New("build failed")
}
return nil
}

func chooseProject(cmd *cobra.Command, projects []detect.Project, root string) (detect.Project, error) {
switch len(projects) {
case 0:
return detect.Project{}, errors.New("no supported project detected; pass --path to a project directory")
case 1:
return projects[0], nil
}
for _, p := range projects {
if p.Path == root {
return p, nil
}
}
out := cmd.ErrOrStderr()
fmt.Fprintln(out, "Multiple projects detected; pass --path to one of:")
for _, p := range projects {
fmt.Fprintf(out, " %s (%s)\n", p.Path, p.Stack)
}
return detect.Project{}, errors.New("multiple projects detected")
}

func printPlan(cmd *cobra.Command, p detect.Project, d driver.Driver, opts driver.BuildOptions) {
out := cmd.OutOrStdout()
fmt.Fprintf(out, "%s (%s) at %s\n\n", d.Name(), p.Subtype, p.Path)
for _, it := range pipeline.Plan(d, opts) {
if it.Step == nil {
fmt.Fprintf(out, "%s: skipped\n", it.Phase)
continue
}
dir := it.Step.Dir
if dir == "" {
dir = "."
}
fmt.Fprintf(out, "%s: %s\n dir: %s\n cmd: %s\n", it.Phase, it.Name, dir, strings.Join(it.Step.Argv, " "))
}
}

func usePlain() bool {
if buildPlain || os.Getenv("CI") != "" {
return true
}
return !term.IsTerminal(int(os.Stdout.Fd()))
}
9 changes: 6 additions & 3 deletions docs/ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,14 @@ High-level phases and status, for monitoring. Granular tasks live in
| Phase | What | Status |
|-------|------|--------|
| 0. Repo and pipeline | Go skeleton, wrapper scripts, Claude hooks, CI, docs, working rules | done |
| 1. Detection | Marker-file detectors for Flutter, React Native, Android, iOS; `anvil detect` | in progress |
| 2. Guided build | Driver lifecycle (deps, analyze, test, build); Flutter first, then RN, Android, iOS; interactive TUI; unified error surfacing | planned |
| 1. Detection | Marker-file detectors for Flutter, React Native, Android, iOS, Swift, Kotlin; `anvil detect` | done |
| 2. Guided build | Driver lifecycle (deps, analyze, test, build) for all six stacks; `--flavor`; interactive TUI plus plain fallback; `anvil build` | in progress |
Comment on lines +9 to +10

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Keep milestone status consistent across the roadmap and progress board.

docs/ROADMAP.md marks Detection as done, while tasks/todo.md still marks Milestone 1 as in progress. Update one source so contributors do not see conflicting project status.

📍 Affects 2 files
  • docs/ROADMAP.md#L9-L10 (this comment)
  • tasks/todo.md#L39-L49
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/ROADMAP.md` around lines 9 - 10, Synchronize the Detection/Milestone 1
status between docs/ROADMAP.md lines 9-10 and tasks/todo.md lines 39-49 so both
sources use the same completed or in-progress state; update only the
inconsistent status entry and preserve the surrounding roadmap and
progress-board content.

| 3. Signing | Guided Android keystore and iOS provisioning/signing | planned |
| 4. Upload | TestFlight, Play, npm upload; GoReleaser distribution (Homebrew, Scoop, curl) | planned |
| 5. Breadth | More ecosystems (web, Go) via new drivers; `--explain` educational mode | future |
| 5. Breadth | More ecosystems (web, Go) via new drivers; flavor auto-detection; `--explain` educational mode | future |

Swift (SPM) and Kotlin/JVM drivers and `--flavor` were pulled forward into
Milestone 2.

## Guiding decisions (locked)

Expand Down
30 changes: 28 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,36 @@ module github.com/openforge-oss/anvil

go 1.26

require github.com/spf13/cobra v1.10.2
require (
github.com/charmbracelet/bubbles v1.0.0
github.com/charmbracelet/bubbletea v1.3.10
github.com/charmbracelet/lipgloss v1.1.0
github.com/spf13/cobra v1.10.2
golang.org/x/term v0.45.0
gopkg.in/yaml.v3 v3.0.1
)

require (
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
github.com/charmbracelet/colorprofile v0.4.1 // indirect
github.com/charmbracelet/x/ansi v0.11.6 // indirect
github.com/charmbracelet/x/cellbuf v0.0.15 // indirect
github.com/charmbracelet/x/term v0.2.2 // indirect
github.com/clipperhouse/displaywidth v0.9.0 // indirect
github.com/clipperhouse/stringish v0.1.1 // indirect
github.com/clipperhouse/uax29/v2 v2.5.0 // indirect
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/lucasb-eyer/go-colorful v1.3.0 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-localereader v0.0.1 // indirect
github.com/mattn/go-runewidth v0.0.19 // indirect
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
github.com/muesli/cancelreader v0.2.2 // indirect
github.com/muesli/termenv v0.16.0 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/spf13/pflag v1.0.9 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
golang.org/x/sys v0.47.0 // indirect
golang.org/x/text v0.3.8 // indirect
)
53 changes: 53 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,12 +1,65 @@
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
github.com/charmbracelet/bubbles v1.0.0 h1:12J8/ak/uCZEMQ6KU7pcfwceyjLlWsDLAxB5fXonfvc=
github.com/charmbracelet/bubbles v1.0.0/go.mod h1:9d/Zd5GdnauMI5ivUIVisuEm3ave1XwXtD1ckyV6r3E=
github.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw=
github.com/charmbracelet/bubbletea v1.3.10/go.mod h1:ORQfo0fk8U+po9VaNvnV95UPWA1BitP1E0N6xJPlHr4=
github.com/charmbracelet/colorprofile v0.4.1 h1:a1lO03qTrSIRaK8c3JRxJDZOvhvIeSco3ej+ngLk1kk=
github.com/charmbracelet/colorprofile v0.4.1/go.mod h1:U1d9Dljmdf9DLegaJ0nGZNJvoXAhayhmidOdcBwAvKk=
github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY=
github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30=
github.com/charmbracelet/x/ansi v0.11.6 h1:GhV21SiDz/45W9AnV2R61xZMRri5NlLnl6CVF7ihZW8=
github.com/charmbracelet/x/ansi v0.11.6/go.mod h1:2JNYLgQUsyqaiLovhU2Rv/pb8r6ydXKS3NIttu3VGZQ=
github.com/charmbracelet/x/cellbuf v0.0.15 h1:ur3pZy0o6z/R7EylET877CBxaiE1Sp1GMxoFPAIztPI=
github.com/charmbracelet/x/cellbuf v0.0.15/go.mod h1:J1YVbR7MUuEGIFPCaaZ96KDl5NoS0DAWkskup+mOY+Q=
github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk=
github.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI=
github.com/clipperhouse/displaywidth v0.9.0 h1:Qb4KOhYwRiN3viMv1v/3cTBlz3AcAZX3+y9OLhMtAtA=
github.com/clipperhouse/displaywidth v0.9.0/go.mod h1:aCAAqTlh4GIVkhQnJpbL0T/WfcrJXHcj8C0yjYcjOZA=
github.com/clipperhouse/stringish v0.1.1 h1:+NSqMOr3GR6k1FdRhhnXrLfztGzuG+VuFDfatpWHKCs=
github.com/clipperhouse/stringish v0.1.1/go.mod h1:v/WhFtE1q0ovMta2+m+UbpZ+2/HEXNWYXQgCt4hdOzA=
github.com/clipperhouse/uax29/v2 v2.5.0 h1:x7T0T4eTHDONxFJsL94uKNKPHrclyFI0lm7+w94cO8U=
github.com/clipperhouse/uax29/v2 v2.5.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g=
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag=
github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
github.com/mattn/go-runewidth v0.0.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw=
github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI=
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo=
github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY=
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI=
golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo=
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs=
golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/term v0.45.0 h1:NwWyBmoJCbfTHpxrWoZ9C6/VxOf7ic219I8xZZFdrf0=
golang.org/x/term v0.45.0/go.mod h1:9aqxs0blBcrm/n0L9QW0aRVD+ktan8ssZromtqJC43w=
golang.org/x/text v0.3.8 h1:nAL+RVCQ9uMn3vJZbV+MRnydTJFPf8qqY42YiA6MrqY=
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
40 changes: 22 additions & 18 deletions internal/detect/android.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,17 @@ package detect
import "path/filepath"

func detectAndroid(dir string) *Project {
hasSettings := fileExists(filepath.Join(dir, "settings.gradle")) ||
fileExists(filepath.Join(dir, "settings.gradle.kts"))
hasBuild := fileExists(filepath.Join(dir, "build.gradle")) ||
fileExists(filepath.Join(dir, "build.gradle.kts"))
if !hasSettings && !hasBuild {
if !hasGradleFiles(dir) {
return nil
}

confidence := 0.6
if hasSettings {
confidence = 0.9
isApp := gradleContains(dir, "com.android.application")
isLibrary := gradleContains(dir, "com.android.library")
if !isApp && !isLibrary {
return nil
}

subtype := "library"
if gradleContains(dir, "com.android.application") {
if isApp {
subtype = "app"
}

Expand All @@ -27,22 +23,30 @@ func detectAndroid(dir string) *Project {
flags = append(flags, "kmp")
}

return &Project{Path: dir, Stack: Android, Subtype: subtype, Confidence: confidence, Flags: flags}
return &Project{Path: dir, Stack: Android, Subtype: subtype, Confidence: 0.9, Flags: flags}
}

func hasGradleFiles(dir string) bool {
for _, n := range []string{"settings.gradle", "settings.gradle.kts", "build.gradle", "build.gradle.kts"} {
if fileExists(filepath.Join(dir, n)) {
return true
}
}
return false
}

// gradleContains scans the build files at dir and in its immediate submodules
// for a token, covering the common case where the application plugin lives in an
// app/ module rather than the root build file.
// for a token, covering the common case where a plugin lives in a submodule
// (for example an app/ or androidApp/ module) rather than the root build file.
func gradleContains(dir, token string) bool {
candidates := []string{
filepath.Join(dir, "build.gradle"),
filepath.Join(dir, "build.gradle.kts"),
}
if entries, err := filepath.Glob(filepath.Join(dir, "*", "build.gradle")); err == nil {
candidates = append(candidates, entries...)
}
if entries, err := filepath.Glob(filepath.Join(dir, "*", "build.gradle.kts")); err == nil {
candidates = append(candidates, entries...)
for _, pat := range []string{"*/build.gradle", "*/build.gradle.kts"} {
if entries, err := filepath.Glob(filepath.Join(dir, pat)); err == nil {
candidates = append(candidates, entries...)
}
}
for _, c := range candidates {
if fileContains(c, token) {
Expand Down
8 changes: 8 additions & 0 deletions internal/detect/detect.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ const (
ReactNative Stack = "react-native"
Android Stack = "android"
IOS Stack = "ios"
Kotlin Stack = "kotlin"
Swift Stack = "swift"
)

type Project struct {
Expand Down Expand Up @@ -98,9 +100,15 @@ func detectDir(dir string) *Project {
if p := detectAndroid(dir); p != nil {
return p
}
if p := detectKotlin(dir); p != nil {
return p
}
if p := detectIOS(dir); p != nil {
return p
}
if p := detectSwift(dir); p != nil {
return p
}
return nil
}

Expand Down
15 changes: 14 additions & 1 deletion internal/detect/detect_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,20 @@ func TestScan(t *testing.T) {
{
name: "swift package library",
files: map[string]string{"Package.swift": "// swift-tools-version:5.9\nlet package = Package(name: \"X\", products: [.library(name: \"X\", targets: [\"X\"])])"},
want: []string{".|ios|library"},
want: []string{".|swift|library"},
},
{
name: "swift executable package",
files: map[string]string{"Package.swift": "// swift-tools-version:5.9\nlet package = Package(name: \"cli\", targets: [.executableTarget(name: \"cli\")])"},
want: []string{".|swift|executable"},
},
{
name: "kotlin jvm gradle is not android",
files: map[string]string{
"settings.gradle.kts": "rootProject.name = \"svc\"",
"build.gradle.kts": "plugins { kotlin(\"jvm\") version \"2.0.0\" }",
},
want: []string{".|kotlin|jvm"},
},
{
name: "node_modules yields no phantom react native projects",
Expand Down
Loading
Loading