diff --git a/cmd/summer/main.go b/cmd/summer/main.go index dc51860..dd90bdd 100644 --- a/cmd/summer/main.go +++ b/cmd/summer/main.go @@ -309,7 +309,8 @@ func newCommand() bonfire.Command { Required: true, }}, Flags: []bonfire.Flag{ - {Name: "framework", Description: "Path to the framework checkout"}, + {Name: "framework", Description: "Path to the framework checkout (default: a sibling summercms or summercms.go checkout)"}, + {Name: "no-plugin", Description: "Do not create the starter plugin", Bare: true}, }, Run: func(ctx context.Context, in bonfire.Input, out bonfire.Output) error { name, ok := in.Argument("name") @@ -325,6 +326,7 @@ func newCommand() bonfire.Command { Name: name, Parent: dir, Framework: framework, + NoPlugin: flagTrue(in, "no-plugin"), }, out) return err }, diff --git a/internal/build/new.go b/internal/build/new.go index 96d34b0..2f32ed8 100644 --- a/internal/build/new.go +++ b/internal/build/new.go @@ -9,6 +9,8 @@ import ( "os" "path/filepath" "regexp" + "runtime" + "strings" ) // appNameRe is the public `summer new ` rule: lower-case letters and @@ -24,10 +26,23 @@ type NewOptions struct { // current directory. Parent string // Framework is the path to the framework checkout, resolved against - // Parent when relative. + // Parent when relative. Empty means auto-detection: a sibling + // summercms or summercms.go checkout, then the source tree this binary + // was built from. Framework string + // NoPlugin skips the starter plugin; summer.yaml then lists no plugins. + NoPlugin bool } +// frameworkSiblings are the directory names auto-detection tries next to +// the new application. A candidate counts only when its go.mod declares the +// framework module. +var frameworkSiblings = []string{"summercms", "summercms.go"} + +// frameworkFromBinary returns the framework checkout this binary was built +// from, or "" when unknown. Tests replace it. +var frameworkFromBinary = binarySourceFramework + // genFile is one file NewApp writes itself, relative to the app root. type genFile struct { Rel string @@ -89,23 +104,24 @@ func NewApp(ctx context.Context, opts NewOptions, out io.Writer) (string, error) if err != nil { return "", fmt.Errorf("build: resolve parent directory: %w", err) } + if st, err := os.Stat(parent); err != nil { + return "", fmt.Errorf("build: parent directory: %w", err) + } else if !st.IsDir() { + return "", fmt.Errorf("build: parent %s is not a directory", parent) + } target := filepath.Join(parent, appDirName(opts.Name)) if _, err := os.Lstat(target); err == nil { return "", fmt.Errorf("build: %s already exists; choose another name or remove it", target) } else if !errors.Is(err, os.ErrNotExist) { return "", fmt.Errorf("build: stat %s: %w", target, err) } - if opts.Framework == "" { - return "", fmt.Errorf("build: framework checkout not found; pass --framework ") - } - framework := opts.Framework - if !filepath.IsAbs(framework) { - framework = filepath.Join(parent, framework) - } - framework = filepath.Clean(framework) - if err := checkFramework(framework); err != nil { + framework, err := resolveFramework(parent, opts.Framework) + if err != nil { return "", err } + if underRoot(framework, target) { + return "", fmt.Errorf("build: %s is inside the framework checkout %s; run summer new in the directory next to the checkout", target, framework) + } files, err := planNewApp(opts, target, framework) if err != nil { @@ -114,21 +130,70 @@ func NewApp(ctx context.Context, opts NewOptions, out io.Writer) (string, error) if err := os.Mkdir(target, 0o755); err != nil { return "", fmt.Errorf("build: create %s: %w", target, err) } + // target did not exist before this run, so a failure removes it and a + // re-run with the same name is not blocked. if err := populateApp(ctx, opts, target, files); err != nil { - return "", err + if rmErr := os.RemoveAll(target); rmErr != nil { + return "", fmt.Errorf("build: create %s: %w (removing it failed: %v)", target, err, rmErr) + } + return "", fmt.Errorf("build: create %s (removed it): %w", target, err) } fmt.Fprintf(out, "created %s\n", target) return target, nil } +// resolveFramework picks the framework checkout: the explicit flag (never +// falling through when it is wrong), then a sibling of parent whose go.mod +// declares the framework module, then the source tree of this binary. Paths +// stay lexical so a symlinked sibling yields a ../ replace. +func resolveFramework(parent, flag string) (string, error) { + if flag != "" { + dir := flag + if !filepath.IsAbs(dir) { + dir = filepath.Join(parent, dir) + } + dir = filepath.Clean(dir) + if err := checkFramework(dir); err != nil { + return "", fmt.Errorf("build: --framework %s: %w", flag, err) + } + return dir, nil + } + for _, name := range frameworkSiblings { + dir := filepath.Join(parent, name) + if checkFramework(dir) == nil { + return dir, nil + } + } + if dir := frameworkFromBinary(); dir != "" && checkFramework(dir) == nil { + return dir, nil + } + return "", fmt.Errorf("build: no framework checkout found: looked for %s next to %s; pass --framework ", + strings.Join(frameworkSiblings, " and "), parent) +} + +// binarySourceFramework returns the repository root this file was compiled +// from. A -trimpath build records a module-relative path, which is ignored. +func binarySourceFramework() string { + _, file, _, ok := runtime.Caller(0) + if !ok || !filepath.IsAbs(file) { + return "" + } + // file is /internal/build/new.go. + root := filepath.Dir(filepath.Dir(filepath.Dir(file))) + if checkFramework(root) != nil { + return "" + } + return root +} + // checkFramework requires dir/go.mod to declare the framework module. func checkFramework(dir string) error { module, _, err := parseGoMod(filepath.Join(dir, "go.mod")) if err != nil { - return fmt.Errorf("build: framework checkout %s: %w", dir, err) + return fmt.Errorf("framework checkout %s: %w", dir, err) } if module != frameworkModule { - return fmt.Errorf("build: %s declares module %q, not the framework %s", dir, module, frameworkModule) + return fmt.Errorf("%s declares module %q, not the framework %s", dir, module, frameworkModule) } return nil } @@ -141,7 +206,7 @@ func planNewApp(opts NewOptions, target, framework string) ([]genFile, error) { return nil, err } if err := checkFramework(framework); err != nil { - return nil, err + return nil, fmt.Errorf("build: %w", err) } replace, err := relModulePath(target, framework) if err != nil { @@ -198,11 +263,10 @@ func populateApp(ctx context.Context, opts NewOptions, target string, files []ge if err := SaveManifest(manifestPath, Manifest{Module: appModulePath(opts.Name), Binary: opts.Name}); err != nil { return err } - if _, err := makePlugin(ctx, target, "golem15."+opts.Name, pluginOptions{StarterRoute: true}); err != nil { - return err - } - if err := AddPlugin(ctx, target, filepath.Join("plugins", opts.Name)); err != nil { - return err + if !opts.NoPlugin { + if err := addStarterPlugin(ctx, opts.Name, target); err != nil { + return err + } } m, err := LoadManifest(manifestPath) if err != nil { @@ -210,3 +274,12 @@ func populateApp(ctx context.Context, opts NewOptions, target string, files []ge } return generate(target, m) } + +// addStarterPlugin creates golem15. through the make:plugin and +// plugin:add code paths, with the starter route. +func addStarterPlugin(ctx context.Context, name, target string) error { + if _, err := makePlugin(ctx, target, "golem15."+name, pluginOptions{StarterRoute: true}); err != nil { + return err + } + return AddPlugin(ctx, target, filepath.Join("plugins", name)) +}