mirror of
https://github.com/chenasraf/sofmani.git
synced 2026-05-17 17:28:04 +00:00
feat: tag field
This commit is contained in:
@@ -130,6 +130,8 @@ If a configuration file is not explicitly provided, `sofmani` attempts to locate
|
||||
|
||||
If no file is found or provided, sofmani will fail to start.
|
||||
|
||||
For more information, see [Configuration Reference](./docs/02-configuration-reference.md)
|
||||
|
||||
---
|
||||
|
||||
## 📚 Configuration Reference
|
||||
|
||||
@@ -1,13 +1,17 @@
|
||||
package appconfig
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/chenasraf/sofmani/platform"
|
||||
"github.com/chenasraf/sofmani/utils"
|
||||
"github.com/samber/lo"
|
||||
)
|
||||
|
||||
type InstallerData struct {
|
||||
Name *string `json:"name" yaml:"name"`
|
||||
Type InstallerType `json:"type" yaml:"type"`
|
||||
Tags *string `json:"tags" yaml:"tags"`
|
||||
Env *map[string]string `json:"env" yaml:"env"`
|
||||
PlatformEnv *platform.PlatformMap[map[string]string] `json:"platform_env" yaml:"platform_env"`
|
||||
Platforms *platform.Platforms `json:"platforms" yaml:"platforms"`
|
||||
@@ -41,3 +45,9 @@ const (
|
||||
func (i *InstallerData) Environ() []string {
|
||||
return utils.EnvMapAsSlice(utils.CombineEnvMaps(i.Env, i.PlatformEnv.Resolve()))
|
||||
}
|
||||
|
||||
func (i *InstallerData) GetTagsList() []string {
|
||||
return lo.Map(strings.Split(*i.Tags, " "), func(tag string, i int) string {
|
||||
return strings.TrimSpace(tag)
|
||||
})
|
||||
}
|
||||
@@ -4,7 +4,8 @@ For a general overview, see the [README](/README.md).
|
||||
|
||||
**Docs Table of Contents**
|
||||
|
||||
- [Getting Started](./01-getting-started.md)
|
||||
- [Configuration Reference](./02-configuration-reference.md)
|
||||
- [Installer Types](./03-installer-types.md)
|
||||
- [Getting Started](./getting-started.md)
|
||||
- [Command Line Interface (CLI)](./command-line-interface.md)
|
||||
- [Configuration Reference](./configuration-reference.md)
|
||||
- [Installer Types](./installer-types.md)
|
||||
- [Recipes](./recipes) - Installer groups you can use immediately as remote manifests
|
||||
|
||||
@@ -1,50 +1,4 @@
|
||||
# Getting Started
|
||||
|
||||
## Installation
|
||||
|
||||
### Download Precompiled Binaries
|
||||
|
||||
Precompiled binaries for `sofmani` are available for **Linux**, **macOS**, and **Windows**:
|
||||
|
||||
- Visit the [Releases Page](https://github.com/chenasraf/sofmani/releases/latest) to download the
|
||||
latest version for your platform.
|
||||
|
||||
### Homebrew (macOS/Linux only)
|
||||
|
||||
Install from a custom tap:
|
||||
|
||||
```bash
|
||||
brew install chenasraf/tap/sofmani
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Linux
|
||||
|
||||
You can install `sofmani` by downloading the release tar, and extracting it to your preferred
|
||||
location.
|
||||
|
||||
- You can see an example script for install here: [install.sh](/install.sh)
|
||||
- The example script can be used for actual install, use this command to download and execute the
|
||||
file (use at your own discretion):
|
||||
|
||||
```sh
|
||||
curl https://raw.githubusercontent.com/chenasraf/sofmani/master/install.sh | sh
|
||||
```
|
||||
|
||||
## Config file location
|
||||
|
||||
The config file can be in YAML or JSON format.
|
||||
|
||||
You can place the config file anywhere, and provide the path to sofmani CLI to load. If you don't
|
||||
give it an explicit path, the CLI will attempt to find a `sofmani.yml` or `sofmani.json` file ine
|
||||
the following directories (ordered by priority):
|
||||
|
||||
1. Current working directory
|
||||
1. `$HOME/.config` directory
|
||||
1. Home directory
|
||||
|
||||
## Using sofmani
|
||||
# Command Line Interface (CLI)
|
||||
|
||||
The sofmani CLI will iterate through each of your install steps (called "Installers") and execute
|
||||
them in sequence.
|
||||
@@ -53,7 +7,7 @@ Installers can be grouped and nested arbitrarily, and loaded either directly fro
|
||||
loaded from an external additional config, either a local file or a remote file hosted on a git
|
||||
repository.
|
||||
|
||||
### CLI Flags
|
||||
## CLI Flags
|
||||
|
||||
You can call `sofmani` with the following flags to alter the behavior for the current run:
|
||||
|
||||
@@ -71,14 +25,32 @@ Each of these flags overrides the loaded config file, so while your default conf
|
||||
check for updates by default, you or another user can add the `--update` flag to override this
|
||||
behavior for a single run of the CLI.
|
||||
|
||||
\* The filter argument accepts multiple values.
|
||||
### Installer Filters
|
||||
|
||||
The filter argument accepts multiple values.
|
||||
|
||||
The following filter types are available:
|
||||
|
||||
- `-f <name>` - filter by name
|
||||
- `-f tag:<tag>` - filter by tag name
|
||||
- `-f type:<type>` - filter by type (brew, shell, etc)
|
||||
|
||||
Each of the above filters can be negated by prefixing with `!`. For example, to exclude installers
|
||||
containing the tag `"system"`, use `-f "!tag:system"`. See more information about tags in the
|
||||
documentation for (Installer Types)[./installer-types.md#fields].
|
||||
|
||||
If there are no filters in the command flags, then all the installers will run.
|
||||
|
||||
If there are filters, each installer will have to match an inclusion filter. Exclusion filters can
|
||||
be combined to then remove from the filtered installers, even if they already matched to be
|
||||
included.
|
||||
|
||||
- To only run installers that contain "sofmani" in their name, use `-f sofmani`.
|
||||
- To run all installers except those that contain "sofmani", use `-f "!sofmani"`.
|
||||
- To only installers that contain "sofmani", but exclude "sofmani-config", use
|
||||
`-f sofmani -f "!sofmani-config"`.
|
||||
- To only installers that contain "sofmani", but exclude ones tagged "config", use
|
||||
`-f sofmani -f "!tag:config"`.
|
||||
|
||||
#### Examples
|
||||
## Examples
|
||||
|
||||
Search for the config in one of the default directories, and enable update checking:
|
||||
|
||||
@@ -92,8 +64,8 @@ Load a specific config file, and enable debug mode:
|
||||
sofmani -d sofmani.yml
|
||||
```
|
||||
|
||||
Load a config file, and only run installers matching `brew` in their name:
|
||||
Load a config file, and only run installers of type `brew`:
|
||||
|
||||
```sh
|
||||
sofmani -f brew sofmani.yml
|
||||
sofmani -f type:brew sofmani.yml
|
||||
```
|
||||
45
docs/getting-started.md
Normal file
45
docs/getting-started.md
Normal file
@@ -0,0 +1,45 @@
|
||||
# Getting Started
|
||||
|
||||
## Installation
|
||||
|
||||
### Download Precompiled Binaries
|
||||
|
||||
Precompiled binaries for `sofmani` are available for **Linux**, **macOS**, and **Windows**:
|
||||
|
||||
- Visit the [Releases Page](https://github.com/chenasraf/sofmani/releases/latest) to download the
|
||||
latest version for your platform.
|
||||
|
||||
### Homebrew (macOS/Linux only)
|
||||
|
||||
Install from a custom tap:
|
||||
|
||||
```bash
|
||||
brew install chenasraf/tap/sofmani
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Linux
|
||||
|
||||
You can install `sofmani` by downloading the release tar, and extracting it to your preferred
|
||||
location.
|
||||
|
||||
- You can see an example script for install here: [install.sh](/install.sh)
|
||||
- The example script can be used for actual install, use this command to download and execute the
|
||||
file (use at your own discretion):
|
||||
|
||||
```sh
|
||||
curl https://raw.githubusercontent.com/chenasraf/sofmani/master/install.sh | sh
|
||||
```
|
||||
|
||||
## Config file location
|
||||
|
||||
The config file can be in YAML or JSON format.
|
||||
|
||||
You can place the config file anywhere, and provide the path to sofmani CLI to load. If you don't
|
||||
give it an explicit path, the CLI will attempt to find a `sofmani.yml` or `sofmani.json` file ine
|
||||
the following directories (ordered by priority):
|
||||
|
||||
1. Current working directory
|
||||
1. `$HOME/.config` directory
|
||||
1. Home directory
|
||||
@@ -20,6 +20,13 @@ These fields are shared by all installer types. Some fields may vary in behavior
|
||||
- **Description**: Type of the step. See [supported types](#supported-type-of-installers) for a
|
||||
comprehensive list of supported values.
|
||||
|
||||
- **`tags`**
|
||||
|
||||
- **Type** String (optional)
|
||||
- **Description**: Arbitrary tags to attach to an installer. These can later be used to filter
|
||||
this installer in or out when running sofmani. This should be a string containing
|
||||
space-separated tags.
|
||||
|
||||
- **`platforms`**
|
||||
|
||||
- **Type**: Object (optional)
|
||||
2
go.mod
2
go.mod
@@ -6,6 +6,7 @@ require (
|
||||
github.com/davecgh/go-spew v1.1.1
|
||||
github.com/eschao/config v0.1.0
|
||||
github.com/fatih/color v1.18.0
|
||||
github.com/samber/lo v1.47.0
|
||||
github.com/stretchr/testify v1.9.0
|
||||
)
|
||||
|
||||
@@ -14,6 +15,7 @@ require (
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
golang.org/x/sys v0.25.0 // indirect
|
||||
golang.org/x/text v0.16.0 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
4
go.sum
4
go.sum
@@ -12,6 +12,8 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/samber/lo v1.47.0 h1:z7RynLwP5nbyRscyvcD043DWYoOcYRv3mV8lBeqOCLc=
|
||||
github.com/samber/lo v1.47.0/go.mod h1:RmDH9Ct32Qy3gduHQuKJ3gW1fMHAnE/fAzQuf6He5cU=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
@@ -20,6 +22,8 @@ golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBc
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34=
|
||||
golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
|
||||
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
|
||||
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.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
|
||||
@@ -2,26 +2,58 @@ package installer
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/samber/lo"
|
||||
)
|
||||
|
||||
func FilterIsMatch(filters []string, name string) bool {
|
||||
func FilterInstaller(installer IInstaller, filters []string) bool {
|
||||
if len(filters) == 0 {
|
||||
return true
|
||||
}
|
||||
match := false
|
||||
for _, f := range filters {
|
||||
if strings.HasPrefix(f, "!") {
|
||||
continue
|
||||
}
|
||||
if strings.Contains(name, f) {
|
||||
match = true
|
||||
positives := lo.Filter(filters, func(filter string, i int) bool {
|
||||
return filter[0] != '!'
|
||||
})
|
||||
|
||||
negatives := lo.FilterMap(filters, func(filter string, i int) (string, bool) {
|
||||
return filter[1:], filter[0] == '!'
|
||||
})
|
||||
|
||||
keep := false
|
||||
if len(positives) == 0 {
|
||||
keep = true
|
||||
}
|
||||
|
||||
for _, f := range positives {
|
||||
if isFilteredIn(installer, f) {
|
||||
keep = true
|
||||
break
|
||||
}
|
||||
}
|
||||
for _, f := range filters {
|
||||
if strings.HasPrefix(f, "!") && strings.Contains(name, f[1:]) {
|
||||
return false
|
||||
for _, f := range negatives {
|
||||
if isFilteredIn(installer, f) {
|
||||
keep = false
|
||||
break
|
||||
}
|
||||
}
|
||||
return match
|
||||
|
||||
return keep
|
||||
}
|
||||
|
||||
func isFilteredIn(installer IInstaller, filter string) bool {
|
||||
data := installer.GetData()
|
||||
if strings.HasPrefix(filter, "type:") {
|
||||
typeName := filter[len("type:"):]
|
||||
if strings.ToLower(string(data.Type)) == strings.ToLower(typeName) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
if strings.HasPrefix(filter, "tag:") {
|
||||
tagName := filter[len("tag:"):]
|
||||
if lo.SomeBy(data.GetTagsList(), func(tag string) bool {
|
||||
return strings.ToLower(tag) == tagName
|
||||
}) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return strings.Contains(*data.Name, filter)
|
||||
}
|
||||
|
||||
@@ -3,29 +3,94 @@ package installer
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/chenasraf/sofmani/appconfig"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestFilterIsMatch(t *testing.T) {
|
||||
func TestFilterInstaller(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
filters []string
|
||||
input string
|
||||
expected bool
|
||||
name string
|
||||
installer IInstaller
|
||||
filters []string
|
||||
expected bool
|
||||
}{
|
||||
{"No filters", []string{}, "test", true},
|
||||
{"Match found", []string{"test"}, "test", true},
|
||||
{"No match", []string{"example"}, "test", false},
|
||||
{"Negation filter", []string{"!test"}, "test", false},
|
||||
{"Negation filter with match", []string{"example", "!test"}, "test", false},
|
||||
{"Negation filter without match", []string{"example", "!test"}, "example", true},
|
||||
{"Negation on included filter", []string{"example", "!example-test"}, "example-test", false},
|
||||
{"Partial match", []string{"config"}, "example-config", true},
|
||||
{
|
||||
name: "No filters",
|
||||
installer: &MockInstaller{
|
||||
data: &appconfig.InstallerData{Name: strPtr("test"), Tags: strPtr("tag1")},
|
||||
},
|
||||
filters: []string{},
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "Positive filter match",
|
||||
installer: &MockInstaller{
|
||||
data: &appconfig.InstallerData{Name: strPtr("test"), Tags: strPtr("tag1")},
|
||||
},
|
||||
filters: []string{"test"},
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "Positive filter no match",
|
||||
installer: &MockInstaller{
|
||||
data: &appconfig.InstallerData{Name: strPtr("test"), Tags: strPtr("tag1")},
|
||||
},
|
||||
filters: []string{"example"},
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "Negative filter match",
|
||||
installer: &MockInstaller{
|
||||
data: &appconfig.InstallerData{Name: strPtr("test"), Tags: strPtr("tag1")},
|
||||
},
|
||||
filters: []string{"!test"},
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "Negative filter no match",
|
||||
installer: &MockInstaller{
|
||||
data: &appconfig.InstallerData{Name: strPtr("test"), Tags: strPtr("tag1")},
|
||||
},
|
||||
filters: []string{"!example"},
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "Tag filter match",
|
||||
installer: &MockInstaller{
|
||||
data: &appconfig.InstallerData{Name: strPtr("test"), Tags: strPtr("tag1")},
|
||||
},
|
||||
filters: []string{"tag:tag1"},
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "Tag filter no match",
|
||||
installer: &MockInstaller{
|
||||
data: &appconfig.InstallerData{Name: strPtr("test"), Tags: strPtr("tag1")},
|
||||
},
|
||||
filters: []string{"tag:tag2"},
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "Type filter match",
|
||||
installer: &MockInstaller{
|
||||
data: &appconfig.InstallerData{Name: strPtr("test"), Type: appconfig.InstallerTypeBrew},
|
||||
},
|
||||
filters: []string{"type:brew"},
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "Type filter no match",
|
||||
installer: &MockInstaller{
|
||||
data: &appconfig.InstallerData{Name: strPtr("test"), Type: appconfig.InstallerTypeBrew},
|
||||
},
|
||||
filters: []string{"type:npm"},
|
||||
expected: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := FilterIsMatch(tt.filters, tt.input)
|
||||
result := FilterInstaller(tt.installer, tt.filters)
|
||||
assert.Equal(t, tt.expected, result)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -112,21 +112,16 @@ func InstallerWithDefaults(
|
||||
|
||||
func FillDefaults(data *appconfig.InstallerData) {
|
||||
if data.Env == nil {
|
||||
env := make(map[string]string)
|
||||
data.Env = &env
|
||||
data.Env = &map[string]string{}
|
||||
}
|
||||
if data.Opts == nil {
|
||||
opts := make(map[string]any)
|
||||
data.Opts = &opts
|
||||
data.Opts = &map[string]any{}
|
||||
}
|
||||
if data.PlatformEnv == nil {
|
||||
mapLinux := map[string]string{}
|
||||
mapMacos := map[string]string{}
|
||||
mapWindows := map[string]string{}
|
||||
env := platform.PlatformMap[map[string]string]{
|
||||
MacOS: &mapMacos,
|
||||
Linux: &mapLinux,
|
||||
Windows: &mapWindows,
|
||||
MacOS: &map[string]string{},
|
||||
Linux: &map[string]string{},
|
||||
Windows: &map[string]string{},
|
||||
}
|
||||
data.PlatformEnv = &env
|
||||
}
|
||||
@@ -135,8 +130,11 @@ func FillDefaults(data *appconfig.InstallerData) {
|
||||
data.Platforms = &platforms
|
||||
}
|
||||
if data.Steps == nil {
|
||||
steps := make([]appconfig.InstallerData, 0)
|
||||
data.Steps = &steps
|
||||
data.Steps = &[]appconfig.InstallerData{}
|
||||
}
|
||||
if data.Tags == nil {
|
||||
str := ""
|
||||
data.Tags = &str
|
||||
}
|
||||
}
|
||||
|
||||
@@ -150,7 +148,7 @@ func RunInstaller(config *appconfig.AppConfig, installer IInstaller) error {
|
||||
logger.Debug("%s should not run on %s, skipping", name, curOS)
|
||||
return nil
|
||||
}
|
||||
if !FilterIsMatch(config.Filter, name) {
|
||||
if !FilterInstaller(installer, config.Filter) {
|
||||
logger.Debug("%s is filtered, skipping", name)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
)
|
||||
|
||||
type MockInstaller struct {
|
||||
info *appconfig.InstallerData
|
||||
data *appconfig.InstallerData
|
||||
isInstalled bool
|
||||
needsUpdate bool
|
||||
installError error
|
||||
@@ -20,7 +20,7 @@ type MockInstaller struct {
|
||||
}
|
||||
|
||||
func (m *MockInstaller) GetData() *appconfig.InstallerData {
|
||||
return m.info
|
||||
return m.data
|
||||
}
|
||||
|
||||
func (m *MockInstaller) CheckIsInstalled() (error, bool) {
|
||||
@@ -63,7 +63,7 @@ func TestInstallerWithDefaults(t *testing.T) {
|
||||
func TestRunInstaller(t *testing.T) {
|
||||
config := &appconfig.AppConfig{}
|
||||
mockInstaller := &MockInstaller{
|
||||
info: &appconfig.InstallerData{Name: strPtr("test"), Type: appconfig.InstallerTypeBrew},
|
||||
data: &appconfig.InstallerData{Name: strPtr("test"), Type: appconfig.InstallerTypeBrew},
|
||||
isInstalled: false,
|
||||
}
|
||||
err := RunInstaller(config, mockInstaller)
|
||||
@@ -72,7 +72,7 @@ func TestRunInstaller(t *testing.T) {
|
||||
|
||||
func TestGetShouldRunOnOS(t *testing.T) {
|
||||
installer := &MockInstaller{
|
||||
info: &appconfig.InstallerData{
|
||||
data: &appconfig.InstallerData{
|
||||
Platforms: &platform.Platforms{
|
||||
Only: &[]platform.Platform{platform.PlatformMacos},
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user