Skip to content

Commit

Permalink
chore: many improvements (#75)
Browse files Browse the repository at this point in the history
* fix: map all ncs versions from toolchain config

* chore: add simplest semver type

It is a simple implementation, that will be sufficient for its usecase

* chore: add more examples

* chore: simplify cmd & config functions

This is the step forward to remove requirement of urfave/cli.

* fix: version-aware template generation

This would allow to generate different things for different toolchain versions.
For example on breaking changes with Zigbee custom implementations.

* feat: generate firmware files for examples when testing

Doing this will be a good way to ensure that examples are staying
in sync with configuration & templates.
  • Loading branch information
ffenix113 authored Jul 28, 2024
1 parent a5f8942 commit ef533cd
Show file tree
Hide file tree
Showing 18 changed files with 402 additions and 99 deletions.
119 changes: 82 additions & 37 deletions cmd/zigbee/firmware/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,15 @@ import (

const filenameArg = "config"

// BuildConfig is aimed to provide build information
// independently of how that information was obtained.
type BuildConfig struct {
WorkDir string
ConfigFile string
OnlyGenerate bool
ClearWorkDir bool
}

func buildCmd() *cli.Command {
return &cli.Command{
Name: "build",
Expand All @@ -29,12 +38,33 @@ func buildCmd() *cli.Command {
Name: "clear-work-dir",
},
},
Action: buildFirmware,
Action: func(ctx *cli.Context) error {
buildCtx, err := newBuildConfigFromCLI(ctx)
if err != nil {
return fmt.Errorf("build context: %w", err)
}

return BuildFirmware(ctx.Context, buildCtx)
},
}
}

func buildFirmware(ctx *cli.Context) error {
cfg, err := parseConfig(ctx)
func newBuildConfigFromCLI(ctx *cli.Context) (BuildConfig, error) {
workdir, err := getWorkdir(ctx)
if err != nil {
return BuildConfig{}, fmt.Errorf("get workdir: %w", err)
}

return BuildConfig{
WorkDir: workdir,
ConfigFile: getConfigFile(ctx),
OnlyGenerate: ctx.Bool("only-generate"),
ClearWorkDir: ctx.Bool("clear-work-dir"),
}, nil
}

func BuildFirmware(ctx context.Context, buildConfig BuildConfig) error {
cfg, err := parseConfig(buildConfig.ConfigFile)
if err != nil {
return fmt.Errorf("prepare config: %w", err)
}
Expand All @@ -43,22 +73,24 @@ func buildFirmware(ctx *cli.Context) error {
return fmt.Errorf("board name cannot be empty")
}

// Will work in the future.
workDir, err := filepath.Abs(ctx.String("workdir"))
if err != nil {
return fmt.Errorf("%w", err)
if err := GenerateFirmwareFiles(ctx, buildConfig.WorkDir, buildConfig.ClearWorkDir, cfg); err != nil {
return fmt.Errorf("generate firmware files: %w", err)
}
if workDir == "" {
workDir = "."

if !buildConfig.OnlyGenerate {
return runBuild(ctx, cfg, buildConfig.WorkDir)
}
workDir = filepath.ToSlash(workDir) // This will make sure that workdir uses slashes as path separators even on windows, which will be fine for cmake.

return nil
}

func GenerateFirmwareFiles(ctx context.Context, workDir string, shouldClearWorkDir bool, cfg *config.Device) error {
generator, err := generate.NewGenerator(cfg)
if err != nil {
return fmt.Errorf("new generator: %w", err)
}

if ctx.Bool("clear-work-dir") {
if shouldClearWorkDir {
if err := clearWorkDir(workDir); err != nil {
return err
}
Expand All @@ -68,15 +100,10 @@ func buildFirmware(ctx *cli.Context) error {
return fmt.Errorf("generate base: %w", err)
}

if !ctx.Bool("only-generate") {
return runBuild(ctx.Context, cfg, workDir)
}

return nil
}

func parseConfig(ctx *cli.Context) (*config.Device, error) {
configPath := getConfigName(ctx)
func parseConfig(configPath string) (*config.Device, error) {
if configPath == "" {
return nil, errors.New("config path cannot be empty (it is set by default)")
}
Expand All @@ -94,25 +121,6 @@ func parseConfig(ctx *cli.Context) (*config.Device, error) {
return conf, nil
}

func getConfigName(ctx *cli.Context) string {
if ctx.IsSet(filenameArg) {
return ctx.String(filenameArg)
}

preferences := []string{"zigbee.yaml", "zigbee.yml"}
for _, preference := range preferences {
if _, err := os.Stat(preference); err == nil {
if preference == "zigbee.yml" {
log.Println("Default config file name changed to 'zigbee.yaml', please change name of your configuration file.")
}

return preference
}
}
// If both files don't exist - return default value.
return "zigbee.yaml"
}

func runBuild(ctx context.Context, device *config.Device, workDir string) error {
build := runner.NewCmd(
"west",
Expand All @@ -128,7 +136,8 @@ func runBuild(ctx context.Context, device *config.Device, workDir string) error
fmt.Sprintf("-DDTC_OVERLAY_FILE=%s/app.overlay", workDir),
)

if err := build.Run(ctx, runner.WithToolchainPath(device.General.GetToochainsPath())); err != nil {
toolchainsPath := device.General.GetToochainsPath()
if err := build.Run(ctx, runner.WithToolchainPath(toolchainsPath.NCS, toolchainsPath.Zephyr)); err != nil {
return fmt.Errorf("build firmware: %w", err)
}

Expand Down Expand Up @@ -157,3 +166,39 @@ func clearWorkDir(workDir string) error {
return os.RemoveAll(path)
})
}

func getWorkdir(ctx *cli.Context) (string, error) {
workDir, err := filepath.Abs(ctx.String("workdir"))
if err != nil {
return "", fmt.Errorf("%w", err)
}
if workDir == "" {
workDir = "."
}

// This will make sure that workdir uses slashes as path separators even on windows,
// which will be fine for cmake.
workDir = filepath.ToSlash(workDir)

return workDir, nil
}

func getConfigFile(ctx *cli.Context) string {
if ctx.IsSet(filenameArg) {
return ctx.String(filenameArg)
}

preferences := []string{"zigbee.yaml", "zigbee.yml"}
for _, preference := range preferences {
if _, err := os.Stat(preference); err == nil {
if preference == "zigbee.yml" {
log.Println("Default config file name changed to 'zigbee.yaml', please change name of your configuration file.")
}

return preference
}
}

// If both files don't exist - return default value.
return "zigbee.yaml"
}
4 changes: 3 additions & 1 deletion cmd/zigbee/firmware/flash.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@ func flashCmd() *cli.Command {
Name: "flash",
Usage: "flash the firmware",
Action: func(ctx *cli.Context) error {
cfg, err := parseConfig(ctx)
configFileName := getConfigFile(ctx)

cfg, err := parseConfig(configFileName)
if err != nil {
return fmt.Errorf("prepare config: %w", err)
}
Expand Down
3 changes: 2 additions & 1 deletion cmd/zigbee/firmware/flasher/mcuboot.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,10 @@ import (
type MCUBoot struct{}

func (MCUBoot) Flash(ctx context.Context, device *config.Device, workDir string) error {
toolchainsPath := device.General.GetToochainsPath()
opts := []runner.CmdOpt{
runner.WithWorkDir(workDir),
runner.WithToolchainPath(device.General.GetToochainsPath()),
runner.WithToolchainPath(toolchainsPath.NCS, toolchainsPath.Zephyr),
}

// 1. Sign the firmware
Expand Down
3 changes: 2 additions & 1 deletion cmd/zigbee/firmware/flasher/west.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,11 @@ func (w West) Flash(ctx context.Context, device *config.Device, workDir string)
opts = append(opts, "--"+opt, fmt.Sprint(val))
}

toolchainsPath := device.General.GetToochainsPath()
return runner.NewCmd("west", opts...).Run(
ctx,
runner.WithWorkDir(workDir),
runner.WithToolchainPath(device.General.GetToochainsPath()),
runner.WithToolchainPath(toolchainsPath.NCS, toolchainsPath.Zephyr),
)
}

Expand Down
8 changes: 6 additions & 2 deletions config/device.go
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ func (d *Device) PrependCommonClusters() {
d.Sensors = slices.Insert(d.Sensors, 0, sensor.Sensor(base.NewCommonDeviceClusters()))
}

func (g General) GetToochainsPath() (string, string) {
func (g General) GetToochainsPath() NCSLocation {
// If env variables are defined - they have higher priority.
ncsToolchainPath := os.Getenv("NCS_TOOLCHAIN_BASE")
ncsVersion := os.Getenv("NCS_VERSION")
Expand Down Expand Up @@ -171,7 +171,11 @@ func (g General) GetToochainsPath() (string, string) {
zephyrPath = locations.Zephyr
}

return ncsToolchainPath, zephyrPath
return NCSLocation{
Version: locations.Version,
NCS: ncsToolchainPath,
Zephyr: zephyrPath,
}
}

func resolveStringEnv(input string) string {
Expand Down
94 changes: 41 additions & 53 deletions config/ncs_finder.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,13 @@ import (
"log"
"os"
"path/filepath"
"regexp"
"sort"
"strconv"

"github.com/ffenix113/zigbee_home/types"
)

type NCSLocation struct {
Version string
Version types.Semver
NCS string
Zephyr string
}
Expand Down Expand Up @@ -70,12 +70,20 @@ func providePaths(ncsBase, version string, toolchainItem toolchainTopLevelItem)
return NCSLocation{}, fmt.Errorf("no toolchain versions found in toolchain configuration path %q", toolchainConfigPath(ncsBase))
}

var availableVersions []string
var availableVersions []types.Semver
for version := range versionToIdentifier {
availableVersions = append(availableVersions, version)
parsed, err := types.ParseSemver(version)
if err != nil {
return NCSLocation{}, fmt.Errorf("parse semver %q: %w", version, err)
}

availableVersions = append(availableVersions, parsed)
}

sort.Strings(availableVersions)
// Sort versions in increasing order
sort.Slice(availableVersions, func(i, j int) bool {
return availableVersions[i].Compare(availableVersions[j]) == -1
})

log.Printf("requested toolchain version: %q, available versions: %v", version, availableVersions)

Expand All @@ -89,85 +97,65 @@ func providePaths(ncsBase, version string, toolchainItem toolchainTopLevelItem)

// If directly requested version is not present - try to use latest from the same minor version.
if bundleID == "" {
version = selectVersion(version, availableVersions)
requiredVersion, err := types.ParseSemver(version)
if err != nil {
return NCSLocation{}, fmt.Errorf("parse required version %q: %w", version, err)
}

foundVersion, err := selectVersion(requiredVersion, availableVersions)
if err != nil {
return NCSLocation{}, fmt.Errorf("select version: %w", err)
}

version = foundVersion.String()
bundleID = versionToIdentifier[version]
}

if bundleID == "" {
return NCSLocation{}, errors.New("required version was not found and no other suitable version is present")
}

return constructPaths(ncsBase, version, bundleID), nil
semver, err := types.ParseSemver(version)
if err != nil {
return NCSLocation{}, fmt.Errorf("parse found version %q: %w", version, err)
}

return constructPaths(ncsBase, semver, bundleID), nil
}

func mapVersions(toolchainItem toolchainTopLevelItem) map[string]string {
mapped := make(map[string]string, len(toolchainItem.Toolchains))

for _, toolchain := range toolchainItem.Toolchains {
mapped[toolchain.NCSVersions[0]] = toolchain.Identifier.BundleID
}

return mapped
}

type version [3]uint8

var versionRegx = regexp.MustCompile(`^v(\d+)\.(\d+)(?:\.(\d+))?$`)

func parseVersion(ver string) version {
match := versionRegx.FindStringSubmatch(ver)
if match == nil {
log.Fatalf("incorrect version %q", ver)
}

var result version
for i, part := range match[1:] {
if i == 2 && part == "" {
part = "0"
for _, version := range toolchain.NCSVersions {
mapped[version] = toolchain.Identifier.BundleID
}

parsed, err := strconv.ParseUint(part, 10, 8)
if err != nil {
log.Fatalf("should not happen: bad part of the version: %q", part)
}

result[i] = uint8(parsed)
}

return result
}

func (v version) greaterOrEqual(other version) bool {
return v[0] == other[0] &&
v[1] == other[1] &&
v[2] >= other[2]
return mapped
}

func selectVersion(requested string, available []string) string {
requestedVer := parseVersion(requested)

func selectVersion(requested types.Semver, available []types.Semver) (types.Semver, error) {
foundIdx := -1
for i, availableVer := range available {
parsedAvailable := parseVersion(availableVer)

if parsedAvailable.greaterOrEqual(requestedVer) {
if availableVer.SameMajorMinor(requested) && availableVer.Compare(requested) > 0 {
foundIdx = i
requestedVer = parsedAvailable
requested = availableVer
}
}

if foundIdx == -1 {
return ""
return types.Semver{}, nil
}

return available[foundIdx]
return available[foundIdx], nil
}

func constructPaths(ncsBase, version, bundleID string) NCSLocation {
func constructPaths(ncsBase string, version types.Semver, bundleID string) NCSLocation {
return NCSLocation{
Version: version,
NCS: filepath.Join(ncsBase, "toolchains", bundleID),
Zephyr: filepath.Join(ncsBase, version, "zephyr"),
Zephyr: filepath.Join(ncsBase, version.String(), "zephyr"),
}
}

Expand Down
Loading

0 comments on commit ef533cd

Please sign in to comment.