feat: manual for individual modules

Refactor CLI to group module related commands together.
Extend HCL writing support to support slices of complex types including slices of blocks with labels.
This commit is contained in:
Peter 2023-06-19 20:19:56 +02:00
parent 0f91cf6c73
commit 73ef9e5135
No known key found for this signature in database
36 changed files with 810 additions and 199 deletions

View file

@ -1,9 +1,9 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="__complete" type="GoApplicationRunConfiguration" factoryName="Go Application">
<module name="buildr" />
<working_directory value="$PROJECT_DIR$/buildr" />
<working_directory value="$PROJECT_DIR$" />
<go_parameters value="--race" />
<parameters value="__complete new task " />
<parameters value="__complete modules man" />
<EXTENSION ID="net.ashald.envfile">
<option name="IS_ENABLED" value="false" />
<option name="IS_SUBST" value="false" />

View file

@ -1,7 +1,7 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="new plugin" type="GoApplicationRunConfiguration" factoryName="Go Application">
<module name="buildr" />
<working_directory value="$PROJECT_DIR$/buildr" />
<working_directory value="$PROJECT_DIR$" />
<parameters value="new task hello_world asdf" />
<EXTENSION ID="net.ashald.envfile">
<option name="IS_ENABLED" value="false" />

View file

@ -1,7 +1,7 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="task go_fmt" type="GoApplicationRunConfiguration" factoryName="Go Application">
<module name="buildr" />
<working_directory value="$PROJECT_DIR$/buildr" />
<working_directory value="$PROJECT_DIR$" />
<parameters value="--pprof.cpu-profile --pprof.out-file cpu.profile task go_fmt" />
<EXTENSION ID="net.ashald.envfile">
<option name="IS_ENABLED" value="false" />

View file

@ -1,7 +1,7 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="task wasi_hello_world" type="GoApplicationRunConfiguration" factoryName="Go Application">
<module name="buildr" />
<working_directory value="$PROJECT_DIR$/buildr" />
<working_directory value="$PROJECT_DIR$" />
<parameters value="--pprof.cpu-profile --pprof.out-file cpu.profile task wasi_hello_world" />
<EXTENSION ID="net.ashald.envfile">
<option name="IS_ENABLED" value="false" />

View file

@ -0,0 +1,16 @@
# {{ .Name }}
{{ .Description }}
{{- if .Examples }}
## Examples
{{ range .Examples }}
### {{ .Name }}
{{ .Description }}
```hcl
{{ to_hcl .Spec }}
```
{{- end }}
{{- end }}

8
go.mod
View file

@ -48,6 +48,9 @@ require (
cloud.google.com/go/iam v1.1.0 // indirect
cloud.google.com/go/storage v1.30.1 // indirect
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect
github.com/Masterminds/goutils v1.1.1 // indirect
github.com/Masterminds/semver/v3 v3.2.0 // indirect
github.com/Masterminds/sprig/v3 v3.2.3 // indirect
github.com/Microsoft/go-winio v0.6.1 // indirect
github.com/ProtonMail/go-crypto v0.0.0-20230528122434-6f98819771a1 // indirect
github.com/acomagu/bufpipe v1.0.4 // indirect
@ -86,6 +89,7 @@ require (
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
github.com/hashicorp/go-safetemp v1.0.0 // indirect
github.com/hashicorp/go-version v1.6.0 // indirect
github.com/huandu/xstrings v1.3.3 // indirect
github.com/imdario/mergo v0.3.16 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
@ -98,9 +102,11 @@ require (
github.com/mattn/go-runewidth v0.0.14 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
github.com/microcosm-cc/bluemonday v1.0.24 // indirect
github.com/mitchellh/copystructure v1.0.0 // indirect
github.com/mitchellh/go-homedir v1.1.0 // indirect
github.com/mitchellh/go-testing-interface v1.14.1 // indirect
github.com/mitchellh/go-wordwrap v1.0.1 // indirect
github.com/mitchellh/reflectwalk v1.0.0 // indirect
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
github.com/muesli/cancelreader v0.2.2 // indirect
github.com/muesli/reflow v0.3.0 // indirect
@ -116,8 +122,10 @@ require (
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/rivo/uniseg v0.4.4 // indirect
github.com/sergi/go-diff v1.3.1 // indirect
github.com/shopspring/decimal v1.2.0 // indirect
github.com/sirupsen/logrus v1.9.3 // indirect
github.com/skeema/knownhosts v1.1.1 // indirect
github.com/spf13/cast v1.3.1 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/ulikunitz/xz v0.5.11 // indirect
github.com/vbatts/tar-split v0.11.3 // indirect

20
go.sum
View file

@ -432,6 +432,12 @@ github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802 h1:1BDTz0u9nC3//pOC
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/DATA-DOG/go-sqlmock v1.5.0 h1:Shsta01QNfFxHCfpW6YH2STWB0MudeXXEWMr20OEh60=
github.com/DATA-DOG/go-sqlmock v1.5.0/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM=
github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI=
github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU=
github.com/Masterminds/semver/v3 v3.2.0 h1:3MEsd0SM6jqZojhjLWWeBY+Kcjy9i6MQAeY7YgDP83g=
github.com/Masterminds/semver/v3 v3.2.0/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ=
github.com/Masterminds/sprig/v3 v3.2.3 h1:eL2fZNezLomi0uOLqjQoN6BfsDD+fyLtgbJMAj9n6YA=
github.com/Masterminds/sprig/v3 v3.2.3/go.mod h1:rXcFaZ2zZbLRJv/xSysmlgIM1u11eBaRMhvYXJNkGuM=
github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY=
github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow=
github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM=
@ -696,6 +702,7 @@ github.com/google/renameio v0.1.0 h1:GOZbcHa3HfsPKPlmyPyN2KEohoMXOhdMbHrvbpl2QaA
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/s2a-go v0.1.4 h1:1kZ/sQM3srePvKs3tXAvQzo66XfcReoqFpIpIccE7Oc=
github.com/google/s2a-go v0.1.4/go.mod h1:Ej+mSEMGRnqRzjc7VtF+jdBwYG5fuJfiZ8ELkjEwM0A=
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
@ -742,9 +749,12 @@ github.com/hashicorp/golang-lru v0.5.1 h1:0hERBMJE1eitiLkihrMvRVBYAkpHzc/J3QdDN+
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/hcl/v2 v2.17.0 h1:z1XvSUyXd1HP10U4lrLg5e0JMVz6CPaJvAgxM0KNZVY=
github.com/hashicorp/hcl/v2 v2.17.0/go.mod h1:gJyW2PTShkJqQBKpAmPO3yxMxIuoXkOF2TpqXzrQyx4=
github.com/huandu/xstrings v1.3.3 h1:/Gcsuc1x8JVbJ9/rlye4xZnVAbEkGauT8lbebqcQws4=
github.com/huandu/xstrings v1.3.3/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639 h1:mV02weKRL81bEnm8A0HT1/CAelMQDBuQIfLw8n+d6xI=
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
github.com/imdario/mergo v0.3.16 h1:wwQJbIsHYGMUyLSPrEq1CT16AhnhNJQ51+4fdHUnCl4=
github.com/imdario/mergo v0.3.16/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
@ -821,6 +831,8 @@ github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfr
github.com/microcosm-cc/bluemonday v1.0.21/go.mod h1:ytNkv4RrDrLJ2pqlsSI46O6IVXmZOBBD4SaJyDwwTkM=
github.com/microcosm-cc/bluemonday v1.0.24 h1:NGQoPtwGVcbGkKfvyYk1yRqknzBuoMiUrO6R7uFTPlw=
github.com/microcosm-cc/bluemonday v1.0.24/go.mod h1:ArQySAMps0790cHSkdPEJ7bGkF2VePWH773hsJNSHf8=
github.com/mitchellh/copystructure v1.0.0 h1:Laisrj+bAB6b/yJwB5Bt3ITZhGJdqmxquMKeZ+mmkFQ=
github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw=
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/go-testing-interface v1.14.1 h1:jrgshOhYAUVNMAJiKbEu7EqAwgJJ2JqpQmpLJOu07cU=
@ -829,6 +841,8 @@ github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQ
github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0=
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/mitchellh/reflectwalk v1.0.0 h1:9D+8oIskB4VJBN5SFlmc27fSlIBZaov1Wpk/IfikLNY=
github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
github.com/mmcloughlin/avo v0.5.0 h1:nAco9/aI9Lg2kiuROBY6BhCI/z0t5jEvJfjWbL8qXLU=
github.com/mmcloughlin/avo v0.5.0/go.mod h1:ChHFdoV7ql95Wi7vuq2YT1bwCJqiWdZrQ1im3VujLYM=
github.com/moby/sys/mountinfo v0.6.2 h1:BzJjoreD5BMFNmD9Rus6gdd1pLuecOFPt8wC+Vygl78=
@ -898,6 +912,8 @@ github.com/sahilm/fuzzy v0.1.0 h1:FzWGaw2Opqyu+794ZQ9SYifWv2EIXpwP4q8dY1kDAwI=
github.com/sahilm/fuzzy v0.1.0/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y=
github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8=
github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I=
github.com/shopspring/decimal v1.2.0 h1:abSATXmQEYyShuxI4/vyW3tV1MrKAJzCZ/0zLUXYbsQ=
github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
@ -907,6 +923,8 @@ github.com/skeema/knownhosts v1.1.1 h1:MTk78x9FPgDFVFkDLTrsnnfCJl7g1C/nnKvePgrIn
github.com/skeema/knownhosts v1.1.1/go.mod h1:g4fPeYpque7P0xefxtGzV81ihjC8sX2IqpAoNkjxbMo=
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72 h1:qLC7fQah7D6K1B0ujays3HV9gkFtllcxhzImRR7ArPQ=
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
github.com/spf13/cast v1.3.1 h1:nFm6S0SMdyzrzcmThSipiEubIDy8WEXKNZ0UOgiRpng=
github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I=
github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
@ -990,6 +1008,7 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20220314234659-1baeb1ce4c0b/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
golang.org/x/crypto v0.9.0 h1:LF6fAI+IutBocDJ2OT0Q1g8plpYljMZ4+lty+dsqw3g=
@ -1485,6 +1504,7 @@ gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRN
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View file

@ -51,6 +51,14 @@ type AppConfigAccessor interface {
AppConfig() AppConfig
}
type TypeRegistryAccessor interface {
TypeRegistry() *modules.TypeRegistry
}
type ModuleRepositoryAccessor interface {
Repository() *modules.Repository
}
type VaultCommander interface {
Init(cfg VaultInitConfig) error
List(writer io.Writer) error
@ -80,6 +88,7 @@ type KnownModulesArgProvider interface {
}
type ManCommander interface {
DisplayModuleManual(pager Pager, cat modules.Category, name string) error
DisplayModulesManual(pager Pager) error
}

View file

@ -4,9 +4,12 @@ import (
"context"
"encoding/json"
"errors"
"fmt"
"os"
"strings"
"code.icb4dc0.de/buildr/buildr/internal/slices"
"code.icb4dc0.de/buildr/buildr/internal/config"
"code.icb4dc0.de/buildr/buildr/internal/plugins"
@ -38,7 +41,6 @@ import (
hcl2 "github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/gohcl"
"github.com/spf13/cobra"
"golang.org/x/exp/maps"
"golang.org/x/exp/slog"
)
@ -82,60 +84,20 @@ func NewApp() *App {
}
app.rootCmd.AddCommand(
ModuleCommand(
modules.CategoryTool,
app,
app.TasksArgsProviderFor(modules.CategoryTool),
WithShort("Run a tool by its name"),
),
ModuleCommand(
modules.CategoryTask,
app,
app.TasksArgsProviderFor(modules.CategoryTask),
WithShort("Run a task by its name"),
),
ModuleCommand(
modules.CategoryBuild,
app,
app.TasksArgsProviderFor(modules.CategoryBuild),
WithShort("Run a build by its name"),
),
ModuleCommand(
modules.CategoryPackage,
app,
app.TasksArgsProviderFor(modules.CategoryPackage),
WithShort("Create a package by its name"),
),
NewCmd(
BootstrapModuleCmd(
modules.CategoryTool,
slices.Map(modules.Categories(), func(c modules.Category) *cobra.Command {
return ExecuteModuleCommand(
c,
app,
app.ModulesArgsProviderFor(modules.CategoryTool),
WithShort("Bootstrap tool module"),
),
BootstrapModuleCmd(
modules.CategoryTask,
app,
app.ModulesArgsProviderFor(modules.CategoryTask),
WithShort("Bootstrap task module"),
),
BootstrapModuleCmd(
modules.CategoryBuild,
app,
app.ModulesArgsProviderFor(modules.CategoryBuild),
WithShort("Bootstrap build module"),
),
BootstrapModuleCmd(
modules.CategoryPackage,
app,
app.ModulesArgsProviderFor(modules.CategoryPackage),
WithShort("Bootstrap package module"),
),
),
TasksArgsProviderFor(app, app, c),
WithShort(fmt.Sprintf("Run a %s by its name", strings.ToLower(c.String()))),
)
})...,
)
app.rootCmd.AddCommand(
VaultCommand(NewVaultApp(app, app, app)),
ServerCommand(NewServerApp(app, app)),
EnvCommand(NewEnvApp(app, app)),
NewManCmd(manApp),
ModulesCommand(app, app, manApp, app),
VersionCommand(),
)
@ -259,6 +221,10 @@ func (a *App) BuildrConfig() config.Buildr {
return *a.buildrCfg
}
func (a *App) Repository() *modules.Repository {
return a.repo
}
func (a *App) BootstrapModule(cat modules.Category, typeName, moduleName string) error {
if err := a.InitAt(InitLevelBuildRConfig); err != nil {
return err
@ -305,56 +271,6 @@ func (a *App) RunModule(ctx context.Context, cat modules.Category, name string)
})
}
func (a *App) ModulesArgsProviderFor(cat modules.Category) KnownModulesArgProvider {
return KnownModulesArgProviderFunc(func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
if err := a.basicParseConfig(cmd.Context()); err != nil {
slog.Warn("Failed to parse config", slog.String("err", err.Error()))
return nil, cobra.ShellCompDirectiveNoFileComp
}
if len(args) != 0 {
return nil, cobra.ShellCompDirectiveNoFileComp
}
knownModules := a.TypeRegistry().Inventory()[cat]
filtered := make([]string, 0, len(knownModules))
toComplete = strings.ToLower(toComplete)
for i := range knownModules {
if strings.HasPrefix(strings.ToLower(knownModules[i]), toComplete) {
filtered = append(filtered, knownModules[i])
}
}
return filtered, cobra.ShellCompDirectiveNoFileComp
})
}
func (a *App) TasksArgsProviderFor(cat modules.Category) KnownTasksArgProvider {
return KnownTasksArgProviderFunc(func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
if err := a.basicParseConfig(cmd.Context()); err != nil {
slog.Warn("Failed to parse config", slog.String("err", err.Error()))
return nil, cobra.ShellCompDirectiveNoFileComp
}
if len(args) != 0 {
return nil, cobra.ShellCompDirectiveNoFileComp
}
tasks := maps.Keys(a.repo.ModulesByCategory(cat))
filtered := make([]string, 0, len(tasks))
for i := range tasks {
if strings.HasPrefix(tasks[i], toComplete) {
filtered = append(filtered, tasks[i])
}
}
return filtered, cobra.ShellCompDirectiveNoFileComp
})
}
// basicParseConfig parses the config files without Vault and API clients.
// It's primary purpose is to be able to list completions
func (a *App) basicParseConfig(ctx context.Context) (err error) {

View file

@ -1,9 +1,13 @@
package cmd
import (
"html/template"
"io"
"io/fs"
"text/template"
"code.icb4dc0.de/buildr/buildr/internal/tmpl"
"github.com/Masterminds/sprig/v3"
"golang.org/x/exp/slog"
"code.icb4dc0.de/buildr/buildr/internal/services"
"code.icb4dc0.de/buildr/buildr/modules"
@ -21,14 +25,18 @@ type Pager interface {
}
func NewManApp(templatesFs fs.FS, svcAcc ManAppServiceAccess) (*ManApp, error) {
tmpl, err := template.New("man").
parsedTemplates, err := template.New("man").
Funcs(sprig.FuncMap()).
Funcs(template.FuncMap{
"to_hcl": tmpl.WriteToHcl,
}).
ParseFS(templatesFs, "manual/*.tmpl.md")
if err != nil {
return nil, err
}
return &ManApp{
templates: tmpl,
templates: parsedTemplates,
serviceAccess: svcAcc,
}, nil
}
@ -38,6 +46,31 @@ type ManApp struct {
serviceAccess ManAppServiceAccess
}
func (m ManApp) DisplayModuleManual(pager Pager, cat modules.Category, name string) error {
mod, err := m.serviceAccess.TypeRegistry().Create(cat, name)
if err != nil {
return err
}
var help modules.Help
if h, ok := mod.Unwrap().(modules.Helper); !ok {
slog.Info(
"Module has no help",
slog.String("category", cat.String()),
slog.String("name", name),
)
return nil
} else {
help = h.Help()
}
if err := m.templates.ExecuteTemplate(pager, "single-module-man.tmpl.md", help); err != nil {
return err
}
return pager.Display()
}
func (m ManApp) DisplayModulesManual(pager Pager) error {
templateData := struct {
Modules map[modules.Category][]string

64
internal/cmd/args.go Normal file
View file

@ -0,0 +1,64 @@
package cmd
import (
"strings"
"code.icb4dc0.de/buildr/buildr/modules"
"github.com/spf13/cobra"
"golang.org/x/exp/maps"
"golang.org/x/exp/slog"
)
func ModulesArgsProviderFor(
initializer LevelInitializer,
registryAcc TypeRegistryAccessor,
cat modules.Category,
) KnownModulesArgProvider {
return KnownModulesArgProviderFunc(func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
if err := initializer.InitAt(InitLevelBasic); err != nil {
slog.Warn("Failed to parse config", slog.String("err", err.Error()))
return nil, cobra.ShellCompDirectiveNoFileComp
}
if len(args) != 0 {
return nil, cobra.ShellCompDirectiveNoFileComp
}
knownModules := registryAcc.TypeRegistry().Inventory()[cat]
filtered := make([]string, 0, len(knownModules))
toComplete = strings.ToLower(toComplete)
for i := range knownModules {
if strings.HasPrefix(strings.ToLower(knownModules[i]), toComplete) {
filtered = append(filtered, knownModules[i])
}
}
return filtered, cobra.ShellCompDirectiveNoFileComp
})
}
func TasksArgsProviderFor(initializer LevelInitializer, repoAcc ModuleRepositoryAccessor, cat modules.Category) KnownTasksArgProvider {
return KnownTasksArgProviderFunc(func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
if err := initializer.InitAt(InitLevelBasic); err != nil {
slog.Warn("Failed to parse config", slog.String("err", err.Error()))
return nil, cobra.ShellCompDirectiveNoFileComp
}
if len(args) != 0 {
return nil, cobra.ShellCompDirectiveNoFileComp
}
tasks := maps.Keys(repoAcc.Repository().ModulesByCategory(cat))
filtered := make([]string, 0, len(tasks))
for i := range tasks {
if strings.HasPrefix(tasks[i], toComplete) {
filtered = append(filtered, tasks[i])
}
}
return filtered, cobra.ShellCompDirectiveNoFileComp
})
}

View file

@ -56,16 +56,17 @@ func EnsureLatestBinary(binariesDir string) (err error) {
}
if info, err = buildinfo.ReadFile(expectedBuildrBinPath); err != nil {
return err
shouldCopy = true
} else {
existingBinaryVCSVersion = settingsToMap(info.Settings)[buildSettingsVCSRevision]
shouldCopy = existingBinaryVCSVersion != thisVCSVersion
}
existingBinaryVCSVersion = settingsToMap(info.Settings)[buildSettingsVCSRevision]
if existingBinaryVCSVersion == thisVCSVersion {
return nil
if shouldCopy {
return copyCurrentBinaryToBinariesDir(expectedBuildrBinPath)
}
return copyCurrentBinaryToBinariesDir(expectedBuildrBinPath)
return nil
}
func copyCurrentBinaryToBinariesDir(expectedBuildrBinPath string) (err error) {

View file

@ -13,7 +13,7 @@ func WithShort(short string) ModuleCommandOption {
}
}
func ModuleCommand(
func ExecuteModuleCommand(
category modules.Category,
cmder ModuleCommander,
argsProvider KnownTasksArgProvider,

View file

@ -5,6 +5,11 @@ import (
"flag"
"fmt"
"code.icb4dc0.de/buildr/buildr/internal/slices"
"code.icb4dc0.de/buildr/buildr/modules"
"github.com/spf13/pflag"
"code.icb4dc0.de/buildr/buildr/internal/config"
"github.com/spf13/cobra"
@ -45,12 +50,27 @@ const (
PagerColorNever PagerColor = "never"
)
type manConfig struct {
func manCfgFromFlags(flags *pflag.FlagSet) (cfg *ManConfig, err error) {
cfg = new(ManConfig)
if cfg.PagerCommand, err = flags.GetString("pager.command"); err != nil {
return nil, err
}
if pagerColor, err := flags.GetString("pager.color"); err != nil {
return nil, err
} else if err = cfg.ColorOutput.Set(pagerColor); err != nil {
return nil, err
}
return cfg, nil
}
type ManConfig struct {
ColorOutput PagerColor
PagerCommand string
}
func (m *manConfig) Flags() *flag.FlagSet {
func (m *ManConfig) Flags() *flag.FlagSet {
fs := flag.NewFlagSet("man", flag.ExitOnError)
fs.StringVar(&m.PagerCommand, "pager.command", config.StringEnvOr("BUILDR_PAGER", ""), "pager command - e.g. bat -l md")
@ -59,31 +79,52 @@ func (m *manConfig) Flags() *flag.FlagSet {
return fs
}
func (m *manConfig) Pager(ctx context.Context) (Pager, error) {
func (m *ManConfig) Pager(ctx context.Context, title string) (Pager, error) {
switch m.PagerCommand {
case "":
return NewTUIPager()
return NewTUIPager(title)
default:
return NewCmdPager(ctx, m.PagerCommand, m.ColorOutput == PagerColorAlways)
}
}
func NewManCmd(cmder ManCommander) *cobra.Command {
var manCfg manConfig
func ModuleManCommand(
category modules.Category,
cmder ManCommander,
argsProvider KnownModulesArgProvider,
manCfg *ManConfig,
) *cobra.Command {
return &cobra.Command{
Use: category.String(),
Short: fmt.Sprintf("Show manual pages for %s modules", category.String()),
SilenceUsage: true,
SilenceErrors: true,
Args: cobra.ExactArgs(1),
ValidArgsFunction: argsProvider.ValidModulesArgs,
RunE: func(cmd *cobra.Command, args []string) (err error) {
p, err := manCfg.Pager(cmd.Context(), fmt.Sprintf("Manual - %s/%s", category.String(), args[0]))
if err != nil {
return err
}
return cmder.DisplayModuleManual(p, category, args[0])
},
}
}
func ManCmd(
cmder ManCommander,
initializer LevelInitializer,
registryAcc TypeRegistryAccessor,
) *cobra.Command {
var manCfg ManConfig
manCmd := &cobra.Command{
Use: "man",
Short: "Show manual pages",
SilenceUsage: true,
SilenceErrors: true,
}
modulesOverviewCmd := &cobra.Command{
Use: "modules",
Short: "Basic overview over all modules",
SilenceUsage: true,
SilenceErrors: true,
RunE: func(cmd *cobra.Command, args []string) error {
p, err := manCfg.Pager(cmd.Context())
p, err := manCfg.Pager(cmd.Context(), "Manual - modules")
if err != nil {
return err
}
@ -91,8 +132,16 @@ func NewManCmd(cmder ManCommander) *cobra.Command {
},
}
manCmd.AddCommand(slices.Map(modules.Categories(), func(c modules.Category) *cobra.Command {
return ModuleManCommand(
c,
cmder,
ModulesArgsProviderFor(initializer, registryAcc, c),
&manCfg,
)
})...)
manCmd.PersistentFlags().AddGoFlagSet(manCfg.Flags())
manCmd.AddCommand(modulesOverviewCmd)
return manCmd
}

39
internal/cmd/modules.go Normal file
View file

@ -0,0 +1,39 @@
package cmd
import (
"fmt"
"strings"
"code.icb4dc0.de/buildr/buildr/internal/slices"
"code.icb4dc0.de/buildr/buildr/modules"
"github.com/spf13/cobra"
)
func ModulesCommand(
initializer LevelInitializer,
registryAcc TypeRegistryAccessor,
manCmder ManCommander,
moduleCmder BootstrapModuleCommander,
) *cobra.Command {
cmd := &cobra.Command{
Use: "modules",
Short: "Interact with modules",
}
cmd.AddCommand(
ManCmd(manCmder, initializer, registryAcc),
NewCmd(
slices.Map(modules.Categories(), func(c modules.Category) *cobra.Command {
return BootstrapModuleCmd(
c,
moduleCmder,
ModulesArgsProviderFor(initializer, registryAcc, c),
WithShort(fmt.Sprintf("Bootstrap %s module", strings.ToLower(c.String()))),
)
})...,
),
)
return cmd
}

View file

@ -24,10 +24,10 @@ func BootstrapModuleCmd(
) *cobra.Command {
cmd := &cobra.Command{
Use: category.String(),
Args: cobra.RangeArgs(1, 2),
SilenceUsage: true,
SilenceErrors: true,
ValidArgsFunction: argsProvider.ValidModulesArgs,
Args: cobra.RangeArgs(1, 2),
RunE: func(cmd *cobra.Command, args []string) error {
var (
typeName = args[0]

View file

@ -20,7 +20,7 @@ var (
_ Pager = (*CommandPager)(nil)
)
func NewTUIPager() (*TUIPager, error) {
func NewTUIPager(title string) (*TUIPager, error) {
renderer, err := glamour.NewTermRenderer(
glamour.WithStyles(glamour.DraculaStyleConfig),
glamour.WithEmoji(),
@ -31,11 +31,13 @@ func NewTUIPager() (*TUIPager, error) {
}
return &TUIPager{
title: title,
renderer: renderer,
}, nil
}
type TUIPager struct {
title string
renderer *glamour.TermRenderer
}
@ -52,9 +54,8 @@ func (t *TUIPager) Display() error {
_, _ = io.Copy(builder, t.renderer)
tuiProgram := tea.NewProgram(
tui.NewPager("Manual - modules", builder.String()),
tui.NewPager(t.title, builder.String()),
tea.WithAltScreen(),
tea.WithMouseCellMotion(),
)
_, err := tuiProgram.Run()

View file

@ -49,8 +49,10 @@ func (c *containerTask) doExecute(ctx context.Context, spec execution.Spec) (err
return err
}
if err = os.MkdirAll(c.moduleWithMeta.OutDir(), 0o755); err != nil {
return err
if outDir := c.moduleWithMeta.OutDir(); outDir != "" {
if err = os.MkdirAll(c.moduleWithMeta.OutDir(), 0o755); err != nil {
return err
}
}
logger := slog.

View file

@ -39,6 +39,26 @@ type TaskFactory struct {
providers []TaskProvider
}
func (f *TaskFactory) TaskForModuleWithoutDependencies(module modules.ModuleWithMeta) (Task, error) {
var t Task
for i := range f.providers {
if p := f.providers[i]; p.CanProvide(module) {
if task, err := p.Create(module); err != nil {
return nil, err
} else {
t = task
}
}
}
if t == nil {
return nil, fmt.Errorf("no provider available to handle module %s", module.Name())
}
return t, nil
}
func (f *TaskFactory) TaskForModule(id string, repo *modules.Repository) (Task, error) {
if t, ok := f.knownTasks[id]; ok {
return t, nil

View file

@ -45,8 +45,10 @@ func (t *localTask) doExecute(ctx context.Context, spec execution.Spec) (err err
}
}()
if err = os.MkdirAll(t.module.OutDir(), 0o755); err != nil {
return err
if outDir := t.module.OutDir(); outDir != "" {
if err = os.MkdirAll(t.module.OutDir(), 0o755); err != nil {
return err
}
}
if mappings := t.module.InputMappings(); len(mappings) > 0 || t.module.Category() == modules.CategoryTool {

View file

@ -6,10 +6,9 @@ import (
"reflect"
"github.com/hashicorp/hcl/v2/hclwrite"
"github.com/zclconf/go-cty/cty"
)
func marshalInto(val reflect.Value, block *hclwrite.Block) error {
func (w *Writer[T]) marshalInto(val reflect.Value, block *hclwrite.Block) error {
if val.CanInterface() {
if marshaler, ok := val.Interface().(Marshaler); ok {
return marshaler.MarshalHCL(block)
@ -17,15 +16,15 @@ func marshalInto(val reflect.Value, block *hclwrite.Block) error {
}
switch val.Kind() {
case reflect.Pointer, reflect.Interface:
return marshalInto(val.Elem(), block)
return w.marshalInto(val.Elem(), block)
case reflect.Struct:
return marshalStructInto(val, block)
return w.marshalStructInto(val, block)
}
return nil
}
func marshalStructInto(val reflect.Value, block *hclwrite.Block) error {
func (w *Writer[T]) marshalStructInto(val reflect.Value, block *hclwrite.Block) error {
if val.CanInterface() {
if marshaler, ok := val.Interface().(Marshaler); ok {
return marshaler.MarshalHCL(block)
@ -34,7 +33,7 @@ func marshalStructInto(val reflect.Value, block *hclwrite.Block) error {
valueType := val.Type()
for i := 0; i < val.NumField(); i++ {
fieldType := valueType.Field(i)
if err := marshalField(fieldType, val.Field(i), block); err != nil {
if err := w.marshalField(fieldType, val.Field(i), block); err != nil {
return err
}
}
@ -42,11 +41,15 @@ func marshalStructInto(val reflect.Value, block *hclwrite.Block) error {
return nil
}
func marshalField(structField reflect.StructField, fieldVal reflect.Value, block *hclwrite.Block) error {
func (w *Writer[T]) marshalField(structField reflect.StructField, fieldVal reflect.Value, block *hclwrite.Block) error {
if !structField.IsExported() {
return nil
}
if w.SkipZeroValues && fieldVal.IsZero() {
return nil
}
marshalCfg, err := marshalConfigOf(structField)
if err != nil && !errors.Is(err, errNoHclTag) {
return err
@ -56,6 +59,32 @@ func marshalField(structField reflect.StructField, fieldVal reflect.Value, block
return nil
}
handleStruct := func(val reflect.Value, t elementType, name string) error {
var (
targetBlock *hclwrite.Block
needsAppending bool
)
switch t {
case elementTypeBlock:
targetBlock = hclwrite.NewBlock(name, discoverLabels(val.Type()))
needsAppending = true
case elementTypeRemain:
targetBlock = block
default:
return fmt.Errorf("undefined block creation behavior for type %s", marshalCfg.Type)
}
if err := w.marshalStructInto(val, targetBlock); err != nil {
return err
}
if needsAppending {
block.Body().AppendBlock(targetBlock)
}
return nil
}
switch fieldVal.Kind() {
case reflect.Map:
if isPrimitiveType(fieldVal.Type().Elem()) {
@ -66,49 +95,39 @@ func marshalField(structField reflect.StructField, fieldVal reflect.Value, block
}
}
case reflect.Slice, reflect.Array:
if isPrimitiveType(fieldVal.Type().Elem()) {
if isPrimitiveType(fieldVal.Type().Elem()) || marshalCfg.Type == elementTypeAttribute {
if v, err := mapToCtyVal(fieldVal, mappingCfg{}); err != nil {
return err
} else {
block.Body().SetAttributeValue(marshalCfg.Name, v)
}
} else {
for i := 0; i < fieldVal.Len(); i++ {
if err := handleStruct(fieldVal.Index(i), elementTypeBlock, marshalCfg.Name); err != nil {
return err
}
}
}
case reflect.Pointer, reflect.Interface:
if fieldVal.IsNil() {
fieldVal = reflect.New(fieldVal.Type().Elem())
}
return marshalField(structField, fieldVal.Elem(), block)
return w.marshalField(structField, fieldVal.Elem(), block)
case reflect.Struct:
if marshalCfg.Type != elementTypeBlock && marshalCfg.Type != elementTypeRemain {
return fmt.Errorf("field %s is a struct but not marked as block", structField.Name)
}
var (
targetBlock *hclwrite.Block
needsAppending bool
)
switch marshalCfg.Type {
case elementTypeBlock:
targetBlock = hclwrite.NewBlock(marshalCfg.Name, discoverLabels(fieldVal.Type()))
needsAppending = true
case elementTypeRemain:
targetBlock = block
default:
return fmt.Errorf("undefined block creation behavior for type %s", marshalCfg.Type)
}
if err := marshalStructInto(fieldVal, targetBlock); err != nil {
return err
}
if needsAppending {
block.Body().AppendBlock(targetBlock)
}
return handleStruct(fieldVal, marshalCfg.Type, marshalCfg.Name)
default:
if val, err := mapToCtyVal(fieldVal, mappingCfg{}); err == nil {
block.Body().SetAttributeValue(marshalCfg.Name, val)
switch marshalCfg.Type {
case elementTypeAttribute:
block.Body().SetAttributeValue(marshalCfg.Name, val)
case elementTypeLabel:
block.SetLabels(append(block.Labels(), val.AsString()))
}
}
}
@ -160,26 +179,3 @@ func isPrimitiveType(t reflect.Type) bool {
return false
}
func fieldCtyValue(val reflect.Value) (cty.Value, bool) {
switch val.Kind() {
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
return cty.NumberUIntVal(val.Uint()), true
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
return cty.NumberIntVal(val.Int()), true
case reflect.Float32, reflect.Float64:
return cty.NumberFloatVal(val.Float()), true
case reflect.String:
return cty.StringVal(val.String()), true
case reflect.Bool:
return cty.BoolVal(val.Bool()), true
case reflect.Pointer:
if val.IsNil() {
val = reflect.New(val.Type().Elem()).Elem()
}
return fieldCtyValue(val)
default:
return cty.Value{}, false
}
}

View file

@ -8,11 +8,30 @@ import (
"github.com/hashicorp/hcl/v2/hclwrite"
)
func NewWriter[T modules.ModuleWithMeta](w io.Writer) *Writer[T] {
return &Writer[T]{writer: w}
type writerCfg struct {
SkipZeroValues bool
}
type WriterOption func(*writerCfg)
func WithSkipZeroValues(skip bool) WriterOption {
return func(cfg *writerCfg) {
cfg.SkipZeroValues = skip
}
}
func NewWriter[T modules.ModuleWithMeta](w io.Writer, opts ...WriterOption) *Writer[T] {
writer := &Writer[T]{writer: w}
for _, opt := range opts {
opt(&writer.writerCfg)
}
return writer
}
type Writer[T modules.ModuleWithMeta] struct {
writerCfg
writer io.Writer
}
@ -20,7 +39,7 @@ func (w *Writer[T]) Write(in T) error {
f := hclwrite.NewEmptyFile()
block := hclwrite.NewBlock(in.Category().String(), []string{in.Type(), in.Name()})
if err := marshalInto(reflect.ValueOf(in), block); err != nil {
if err := w.marshalInto(reflect.ValueOf(in), block); err != nil {
return err
}

9
internal/slices/map.go Normal file
View file

@ -0,0 +1,9 @@
package slices
func Map[TIn any, TOut any](slice []TIn, f func(TIn) TOut) []TOut {
out := make([]TOut, len(slice))
for i, v := range slice {
out[i] = f(v)
}
return out
}

View file

@ -0,0 +1,19 @@
package tmpl
import (
"strings"
"code.icb4dc0.de/buildr/buildr/internal/hcl"
"code.icb4dc0.de/buildr/buildr/modules"
)
func WriteToHcl(mod modules.ModuleWithMeta) (string, error) {
builder := new(strings.Builder)
writer := hcl.NewWriter[modules.ModuleWithMeta](builder, hcl.WithSkipZeroValues(true))
if err := writer.Write(mod); err != nil {
return "", err
}
return builder.String(), nil
}

View file

@ -47,6 +47,10 @@ type Initializer interface {
Init(hclCtx *hcl.EvalContext) (Module, error)
}
type Helper interface {
Help() Help
}
type Module interface {
Execute(ctx ExecutionContext) error
Category() Category

View file

@ -12,7 +12,10 @@ import (
const defaultArgsLength = 6
var _ modules.Module = (*GoBuild)(nil)
var (
_ modules.Module = (*GoBuild)(nil)
_ modules.Helper = (*GoBuild)(nil)
)
type GoBuild struct {
Binary string `hcl:"binary"`

View file

@ -0,0 +1,106 @@
package golang
import "code.icb4dc0.de/buildr/buildr/modules"
func (g GoBuild) Help() modules.Help {
return modules.Help{
Name: "Go build - build Go binaries",
Description: `This module helps to build Go binaries.
It abstracts common build parameters like GOOS and GOARCH.
Less common parameters can be specified e.g. with ` + "`flags`" + ` or ` + "`ldflags`" + `.
Builds - as every other task - are executed in parallel.`,
Examples: []modules.Example{
{
Name: "Simple build",
Description: "Simplest possible example of a build",
Spec: &modules.Metadata[GoBuild]{
ModuleName: "buildr_linux_amd64",
Module: GoBuild{
Binary: "buildr",
Main: ".",
GoOS: "linux",
GoArch: "amd64",
},
},
},
{
Name: "Build with flags",
Description: `Specify normal build flags as well as ldflags.`,
Spec: &modules.Metadata[GoBuild]{
ModuleName: "buildr_linux_amd64",
Module: GoBuild{
Binary: "buildr",
Main: ".",
GoOS: "linux",
GoArch: "amd64",
Flags: []string{
"-v",
"-trimpath",
"-a",
"-installsuffix=cgo",
},
LdFlags: []string{
"-w -s",
"-X 'code.icb4dc0.de/buildr/buildr/cmd.CurrentVersion=main'",
},
},
},
},
{
Name: "Build with environment variables",
Description: `Specify additional environment variables.`,
Spec: &modules.Metadata[GoBuild]{
ModuleName: "buildr_linux_amd64",
Module: GoBuild{
Binary: "buildr",
Main: ".",
GoOS: "linux",
GoArch: "amd64",
Env: map[string]string{
"CGO_ENABLED": "0",
},
},
},
},
{
Name: "Containerize build process",
Description: `It is also possible to run the build in a container.
Although it is not very effective because the build process always has to download all dependencies if they are not cached.`,
Spec: &modules.Metadata[GoBuild]{
ModuleName: "buildr_linux_amd64",
Container: &modules.ContainerSpec{
Image: "golang:alpine",
},
Module: GoBuild{
Binary: "buildr",
Main: ".",
GoOS: "linux",
GoArch: "amd64",
},
},
},
{
Name: "Containerize build process - with module cache volume",
Description: `Specify additional environment variables.`,
Spec: &modules.Metadata[GoBuild]{
ModuleName: "buildr_linux_amd64",
Container: &modules.ContainerSpec{
Image: "golang:alpine",
VolumeMounts: []modules.ContainerVolumeMount{
{
Target: "/go/pkg/mod",
Name: "modcache",
},
},
},
Module: GoBuild{
Binary: "buildr",
Main: ".",
GoOS: "linux",
GoArch: "amd64",
},
},
},
},
}
}

13
modules/help.go Normal file
View file

@ -0,0 +1,13 @@
package modules
type Example struct {
Name string
Description string
Spec ModuleWithMeta
}
type Help struct {
Name string
Description string
Examples []Example
}

View file

@ -0,0 +1,40 @@
package state
import (
"context"
"sync"
)
var _ Store = (*InMemoryStore)(nil)
type InMemoryStore struct {
lock sync.RWMutex
data map[string][]byte
}
func (i *InMemoryStore) Set(_ context.Context, key Key, state []byte, opts ...EntryOption) error {
i.lock.Lock()
defer i.lock.Unlock()
if i.data == nil {
i.data = make(map[string][]byte)
}
i.data[string(key.Bytes())] = state
return nil
}
func (i *InMemoryStore) Get(ctx context.Context, key Key) (state []byte, meta Metadata, err error) {
i.lock.RLock()
defer i.lock.RUnlock()
if i.data == nil {
return nil, Metadata{}, nil
}
if d, ok := i.data[string(key.Bytes())]; ok {
return d, Metadata{}, nil
} else {
return nil, Metadata{}, nil
}
}

View file

@ -15,7 +15,10 @@ import (
"code.icb4dc0.de/buildr/buildr/modules"
)
var _ modules.Module = (*ScriptTask)(nil)
var (
_ modules.Module = (*ScriptTask)(nil)
_ modules.Helper = (*ScriptTask)(nil)
)
type ScriptTask struct {
Shell string `hcl:"shell,optional"`

View file

@ -0,0 +1,96 @@
package task
import (
"code.icb4dc0.de/buildr/buildr/modules"
)
func (s ScriptTask) Help() modules.Help {
return modules.Help{
Name: "Script - run single commands or complete scripts",
Description: `This module helps to run arbitrary commands or complete scripts.
It's possible to either inline all commands or specify an external script file.
Furthermore, it's possible to specify which shell should be used to execute the commands.
Right now this will only work with Linux/MacOS shells.`,
Examples: []modules.Example{
{
Name: "Hello world",
Description: `Simples possible script.`,
Spec: &modules.Metadata[ScriptTask]{
ModuleName: "hello_world",
Module: ScriptTask{
Inline: []string{`echo 'Hello world!'`},
},
},
},
{
Name: "Multiline inline script",
Description: `Specify multiple array entries to run a script.
All lines of the inline script will be joined to a single string so it's possible to use some context like changing the current working directory.`,
Spec: &modules.Metadata[ScriptTask]{
ModuleName: "multi_inline_script",
Module: ScriptTask{
Inline: []string{
`mkdir test`,
`echo 'Hello world' > test/sample.txt`,
`cd test`,
`cat sample.txt`,
},
},
},
},
{
Name: "External script file",
Description: `Call a script from a file.`,
Spec: &modules.Metadata[ScriptTask]{
ModuleName: "external_script_file",
Module: ScriptTask{
Script: "testdata/sample.sh",
},
},
},
{
Name: "Manipulate environment variables",
Description: `Set environment variables and use it in a script`,
Spec: &modules.Metadata[ScriptTask]{
ModuleName: "env_script",
Module: ScriptTask{
Inline: []string{
`echo "$GREETING world"`,
},
Env: map[string]string{
"GREETING": "Hello",
},
},
},
},
{
Name: "Change working directory",
Description: `Set the working directory to another directory.`,
Spec: &modules.Metadata[ScriptTask]{
ModuleName: "script_with_working_dir",
Module: ScriptTask{
Inline: []string{
`cat sample.txt`,
},
WorkingDir: "testdata",
},
},
},
{
Name: "Container script",
Description: `Run a script inside a container.`,
Spec: &modules.Metadata[ScriptTask]{
ModuleName: "container_script",
Container: &modules.ContainerSpec{
Image: "busybox",
},
Module: ScriptTask{
Inline: []string{
`echo 'Hello, world!'`,
},
},
},
},
},
}
}

View file

@ -0,0 +1,43 @@
package task_test
import (
"context"
"os"
"testing"
"code.icb4dc0.de/buildr/buildr/modules"
"code.icb4dc0.de/buildr/buildr/modules/state"
"code.icb4dc0.de/buildr/buildr/modules/task"
)
func TestExamples(t *testing.T) {
cwd, err := os.Getwd()
if err != nil {
t.Fatal(err)
}
s := new(task.ScriptTask)
for _, e := range s.Help().Examples {
e := e
t.Run(e.Name, func(t *testing.T) {
var memStateStore state.InMemoryStore
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
execCtx := modules.TestExecutionContext{
Context: ctx,
TB: t,
TestWorkingDir: t.TempDir(),
OutputDirectory: t.TempDir(),
StateStore: &memStateStore,
}
if e.Spec.Unwrap().(task.ScriptTask).WorkingDir != "" {
execCtx.TestWorkingDir = cwd
}
if err := e.Spec.Execute(execCtx); err != nil {
t.Errorf("failed to execute example: %s: %v", e.Name, err)
}
})
}
}

3
modules/task/testdata/sample.sh vendored Normal file
View file

@ -0,0 +1,3 @@
#!/usr/bin/env bash
echo "Hello, world!"

1
modules/task/testdata/sample.txt vendored Normal file
View file

@ -0,0 +1 @@
Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.

67
modules/test_ctx.go Normal file
View file

@ -0,0 +1,67 @@
package modules
import (
"context"
"io"
"testing"
"code.icb4dc0.de/buildr/buildr/modules/state"
"golang.org/x/exp/slog"
)
var _ ExecutionContext = (*TestExecutionContext)(nil)
type TestExecutionContext struct {
context.Context
TB testing.TB
TestWorkingDir, OutputDirectory string
StateStore state.Store
}
func (t TestExecutionContext) Name() string {
return t.TB.Name()
}
func (t TestExecutionContext) WorkingDir() string {
return t.TestWorkingDir
}
func (t TestExecutionContext) OutDir() string {
return t.OutputDirectory
}
func (t TestExecutionContext) BinariesDir() string {
return ""
}
func (t TestExecutionContext) StdOut() io.Writer {
return testWriter{TB: t.TB}
}
func (t TestExecutionContext) StdErr() io.Writer {
return testWriter{TB: t.TB}
}
func (t TestExecutionContext) Logger() *slog.Logger {
return slog.New(slog.NewTextHandler(testWriter{TB: t.TB}, nil))
}
func (t TestExecutionContext) GetState(ctx context.Context, key string) ([]byte, state.Metadata, error) {
return t.StateStore.Get(ctx, state.KeyOfStrings(key))
}
func (t TestExecutionContext) SetState(ctx context.Context, key string, value []byte) error {
return t.StateStore.Set(ctx, state.KeyOfStrings(key), value)
}
var _ io.Writer = (*testWriter)(nil)
type testWriter struct {
testing.TB
}
func (t testWriter) Write(p []byte) (n int, err error) {
t.Log(string(p))
return len(p), nil
}

View file

@ -18,3 +18,12 @@ const (
CategoryBuild Category = "build"
CategoryPackage Category = "package"
)
func Categories() []Category {
return []Category{
CategoryTool,
CategoryTask,
CategoryBuild,
CategoryPackage,
}
}