Skip to content

Commit

Permalink
Run custom commands at build time
Browse files Browse the repository at this point in the history
This commit adds the ability to run custom commands at build time.

A package specifies custom commands in its `pkg.yml` file.  There are
two types of commands: 1) pre_cmds (run before the build), and 2)
post_cmds (run after the build).

EXAMPLE

Example (apps/blinky/pkg.yml):

    pkg.pre_cmds:
        scripts/pre_build1.sh: 100
        scripts/pre_build2.sh: 200

    pkg.post_cmds:
        scripts/post_build.sh: 100

For each command, the string on the left specifies the command to run.
The number on the right indicates the command's relative ordering.

When newt builds this example, it performs the following sequence:

    scripts/pre_build1.sh
    scripts/pre_build2.sh
    [compile]
    [link]
    scripts/post_build.sh

If other packages specify custom commands, those commands would also be
executed during the above sequence.  For example, if another package
specifies a pre command with an ordering of 150, that command would run
immediately after `pre_build1.sh`.  In the case of a tie, the commands
are run in lexicographic order.

All commands are run from the project's base directory.  In the above
example, the `scripts` directory would be a sibling of `targets`.

CUSTOM BUILD INPUTS

A custom pre-build command can produce files that get fed into the
current build.  A command can generate any of the following:

1) .c files for newt to compile.
2) .a files for newt to link.
3) .h files that any package can include.

.c and .a files should be written to "$MYNEWT_USER_SRC_DIR" (or any
subdirectory within).

.h files should be written to "$MYNEWT_USER_INCLUDE_DIR".  The directory
structure used here is directly reflected by the includer.  E.g., if a
script writes to:

    $MYNEWT_USER_INCLUDE_DIR/foo/bar.h

then a source file can include this header with:

    #include "foo/bar.h"

DETAILS

1. Environment variables

In addition to the usual environment variables defined for debug and
download scripts, newt defines the following env vars for custom
commands:

* MYNEWT_USER_SRC_DIR:     Path where build inputs get written.
* MYNEWT_USER_INCLUDE_DIR: Path where globally-accessible headers get
                               written.
* MYNEWT_PKG_NAME:         The full name of the package that specifies
                               the command being executed.
* MYNEWT_APP_BIN_DIR:      The path where the current target's binary
                               gets written.

These environment variables are defined for each processes that a custom
command runs in.  They are *not* defined in the newt process itself.
So, the following snippet will not produce the expected output:

BAD Example (apps/blinky/pkg.yml):

    pkg.pre_cmds:
        'echo $MYNEWT_USER_SRC_DIR': 100

You can execute `sh` here instead if you need access to the environment
variables, but it is probably saner to just use a script.

2. Detect changes in custom build inputs

To avoid unnecessary rebuilds, newt detects if custom build inputs have
changed since the previous build.  If none of the inputs have changed,
then they do not get rebuilt.  If any of them of them have changed, they
all get rebuilt.

The $MYNEWT_USER_[...] base is actually a temp directory.  After the
pre-build commands have run, newt compares the contents of the temp
directory with those of the actual user directory.  If any differences
are detected, newt replaces the user directory with the temp directory,
triggering a rebuild of its contents.

3. Paths

Custom build inputs get written to the following directories:

* bin/targets/<target>/user/src
* bin/targets/<target>/user/include

Custom commands should not write to these directories.  They should use
the $MYNEWT_USER_[...] environment variables instead.
  • Loading branch information
ccollins476ad committed Sep 23, 2019
1 parent bd8f133 commit 035787e
Show file tree
Hide file tree
Showing 13 changed files with 493 additions and 23 deletions.
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ go 1.12
require (
github.com/NickBall/go-aes-key-wrap v0.0.0-20170929221519-1c3aa3e4dfc5
github.com/apache/mynewt-artifact v0.0.3
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51
github.com/sirupsen/logrus v1.4.2
github.com/spf13/cast v1.3.0
github.com/spf13/cobra v0.0.5
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
Expand Down
34 changes: 28 additions & 6 deletions newt/builder/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,10 +83,13 @@ func NewBuilder(
}
}

// Create a pseudo build package for the generated sysinit code.
// Create the pseudo build packages.
if _, err := b.addSysinitBpkg(); err != nil {
return nil, err
}
if _, err := b.addUserBpkg(); err != nil {
return nil, err
}

for api, rpkg := range apiMap {
bpkg := b.PkgMap[rpkg]
Expand Down Expand Up @@ -504,6 +507,10 @@ func (b *Builder) PrepBuild() error {
baseCi.Includes = append(baseCi.Includes,
GeneratedIncludeDir(b.targetPkg.rpkg.Lpkg.Name()))

// All packages have access to the user generated header directory.
baseCi.Includes = append(baseCi.Includes,
UserIncludeDir(b.targetPkg.rpkg.Lpkg.Name()))

// Let multiplatform libraries know that a Mynewt binary is being build.
baseCi.Cflags = append(baseCi.Cflags, "-DMYNEWT=1")

Expand All @@ -518,17 +525,32 @@ func (b *Builder) AddCompilerInfo(info *toolchain.CompilerInfo) {
b.compilerInfo.AddCompilerInfo(info)
}

func (b *Builder) addSysinitBpkg() (*BuildPackage, error) {
lpkg := pkg.NewLocalPackage(b.targetPkg.rpkg.Lpkg.Repo().(*repo.Repo),
GeneratedBaseDir(b.targetPkg.rpkg.Lpkg.Name()))
lpkg.SetName(pkg.ShortName(b.targetPkg.rpkg.Lpkg) + "-sysinit-" +
b.buildName)
// addPseudoBpkg creates a dynamic build package and adds it to the builder.
func (b *Builder) addPseudoBpkg(name string,
dir string) (*BuildPackage, error) {

lpkg := pkg.NewLocalPackage(b.targetPkg.rpkg.Lpkg.Repo().(*repo.Repo), dir)
lpkg.SetName(fmt.Sprintf(
"%s-%s", pkg.ShortName(b.targetPkg.rpkg.Lpkg), name))
lpkg.SetType(pkg.PACKAGE_TYPE_GENERATED)

rpkg := resolve.NewResolvePkg(lpkg)
return b.addPackage(rpkg)
}

// addSysinitBpkg adds the pseudo sysinit build package to the builder.
func (b *Builder) addSysinitBpkg() (*BuildPackage, error) {
name := fmt.Sprintf("%s-%s", "sysinit", b.buildName)
dir := GeneratedBaseDir(b.targetPkg.rpkg.Lpkg.Name())
return b.addPseudoBpkg(name, dir)
}

// addSysinitBpkg adds the pseudo user build package to the builder. The user
// build package contains inputs emitted by external scripts.
func (b *Builder) addUserBpkg() (*BuildPackage, error) {
return b.addPseudoBpkg("user", UserBaseDir(b.targetPkg.rpkg.Lpkg.Name()))
}

// Runs build jobs while any remain. On failure, signals the other workers to
// stop via the stop channel. On error, the error object is signaled via the
// results channel. On successful completion, nil is signaled via the results
Expand Down
34 changes: 33 additions & 1 deletion newt/builder/buildutil.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ package builder

import (
"bytes"
"fmt"
"sort"
"strconv"
"strings"
Expand Down Expand Up @@ -217,6 +218,19 @@ func SlotEnvVars(bspPkg *pkg.BspPackage,
return env, nil
}

// UserEnvVars calculates the set of environment variables required by external
// user scripts.
func UserEnvVars(tempDir string, lpkg *pkg.LocalPackage, targetName string,
appName string) map[string]string {

return map[string]string{
"MYNEWT_USER_SRC_DIR": UserTempSrcDir(tempDir),
"MYNEWT_USER_INCLUDE_DIR": UserTempIncludeDir(tempDir),
"MYNEWT_PKG_NAME": lpkg.FullName(),
"MYNEWT_APP_BIN_DIR": FileBinDir(targetName, "app", appName),
}
}

// EnvVars calculates the full set of environment variables passed to external
// scripts.
func (b *Builder) EnvVars(imageSlot int) (map[string]string, error) {
Expand All @@ -238,7 +252,7 @@ func (b *Builder) EnvVars(imageSlot int) (map[string]string, error) {
imageSlot = -1
}

slotEnv, err := SlotEnvVars(bspPkg, imageSlot, settings)
slotEnv, err := SlotEnvVars(bspPkg, imageSlot)
if err != nil {
return nil, err
}
Expand All @@ -252,3 +266,21 @@ func (b *Builder) EnvVars(imageSlot int) (map[string]string, error) {

return env, nil
}

// EnvVarsToSlice converts an environment variable map into a slice of strings
// suitable for "shell command" functions defined in `util` (e.g.,
// util.ShellCommand).
func EnvVarsToSlice(env map[string]string) []string {
keys := make([]string, 0, len(env))
for k, _ := range env {
keys = append(keys, k)
}
sort.Strings(keys)

slice := make([]string, 0, len(env))
for _, key := range keys {
slice = append(slice, fmt.Sprintf("%s=%s", key, env[key]))
}

return slice
}
135 changes: 135 additions & 0 deletions newt/builder/extcmd.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
package builder

import (
"io/ioutil"
"os"
"os/exec"

"github.com/kballard/go-shellquote"
log "github.com/sirupsen/logrus"
"mynewt.apache.org/newt/newt/stage"
"mynewt.apache.org/newt/util"
)

// artifactsAreSame indicates whether the artifacts produced by the set of
// pre-build user scripts are identical to those from the previous build. If
// the artifacts are unchanged, there is no need to pass updated copies into
// the build.
func artifactsAreSame(fromSrc string, fromInc string,
toSrc string, toInc string) (bool, error) {

srcEq, err := util.DirsAreEqual(fromSrc, toSrc)
if err != nil {
return false, err
}
if !srcEq {
return false, nil
}

incEq, err := util.DirsAreEqual(fromInc, toInc)
if err != nil {
return false, err
}
if !incEq {
return false, nil
}

return true, nil
}

// execExtCmds executes a set of user scripts.
func execExtCmds(sf stage.StageFunc, env map[string]string) error {
envs := EnvVarsToSlice(env)
toks, err := shellquote.Split(sf.Name)
if err != nil {
return util.FmtNewtError(
"invalid command string: \"%s\": %s", sf.Name, err.Error())
}

// If the command is in the user's PATH, expand it to its real location.
cmd, err := exec.LookPath(toks[0])
if err == nil {
toks[0] = cmd
}

if err := util.ShellInteractiveCommand(toks, envs, true); err != nil {
return err
}

return nil
}

// execPreCmds runs the target's set of pre-build user commands. It is an
// error if any command fails (exits with a nonzero status).
func (t *TargetBuilder) execPreCmds(env map[string]string) error {
// Create temporary directories where scripts can put build inputs.
tmpDir, err := ioutil.TempDir("", "mynewt-user")
if err != nil {
return util.ChildNewtError(err)
}
defer func() {
log.Debugf("removing user dir: %s", tmpDir)
os.RemoveAll(tmpDir)
}()

tmpSrcDir := UserTempSrcDir(tmpDir)
log.Debugf("creating user src dir: %s", tmpSrcDir)
if err := os.MkdirAll(tmpSrcDir, 0755); err != nil {
return util.ChildNewtError(err)
}

tmpIncDir := UserTempIncludeDir(tmpDir)
log.Debugf("creating user include dir: %s", tmpIncDir)
if err := os.MkdirAll(tmpIncDir, 0755); err != nil {
return util.ChildNewtError(err)
}

for _, sf := range t.res.PreCmdCfg.StageFuncs {
uenv := UserEnvVars(tmpDir, sf.Pkg, t.target.Name(),
t.appPkg.FullName())
for k, v := range uenv {
env[k] = v
}
if err := execExtCmds(sf, env); err != nil {
return err
}
}

// Compare the artifacts just produced (temp directory) to those from the
// previous build (user bin directory). If they are different, replace the
// old with the new so that they get relinked during this build.

srcDir := UserSrcDir(t.target.Name())
incDir := UserIncludeDir(t.target.Name())

eq, err := artifactsAreSame(srcDir, incDir, tmpSrcDir, tmpIncDir)
if err != nil {
return err
}

if !eq {
os.RemoveAll(srcDir)
if err := os.Rename(tmpSrcDir, srcDir); err != nil {
return util.ChildNewtError(err)
}

os.RemoveAll(incDir)
if err := os.Rename(tmpIncDir, incDir); err != nil {
return util.ChildNewtError(err)
}
}

return nil
}

// execPostCmds runs the target's set of post-build user commands. It is an
// error if any command fails (exits with a nonzero status).
func (t *TargetBuilder) execPostCmds(env map[string]string) error {
for _, sf := range t.res.PostCmdCfg.StageFuncs {
if err := execExtCmds(sf, env); err != nil {
return err
}
}

return nil
}
13 changes: 1 addition & 12 deletions newt/builder/load.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ import (
"fmt"
"os"
"os/signal"
"sort"
"strings"
"syscall"

Expand Down Expand Up @@ -126,17 +125,7 @@ func Load(binBasePath string, bspPkg *pkg.BspPackage,
for k, v := range extraEnvSettings {
env[k] = v
}

sortedKeys := make([]string, 0, len(env))
for k, _ := range env {
sortedKeys = append(sortedKeys, k)
}
sort.Strings(sortedKeys)

envSlice := []string{}
for _, key := range sortedKeys {
envSlice = append(envSlice, fmt.Sprintf("%s=%s", key, env[key]))
}
envSlice := EnvVarsToSlice(env)

RunOptionalCheck(bspPkg.OptChkScript, envSlice)
// bspPath, binBasePath are passed in command line for backwards
Expand Down
20 changes: 20 additions & 0 deletions newt/builder/paths.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,26 @@ func SysinitArchivePath(targetName string) string {
return GeneratedBinDir(targetName) + "/sysinit.a"
}

func UserBaseDir(targetName string) string {
return BinRoot() + "/" + targetName + "/user"
}

func UserSrcDir(targetName string) string {
return UserBaseDir(targetName) + "/src"
}

func UserIncludeDir(targetName string) string {
return UserBaseDir(targetName) + "/include"
}

func UserTempSrcDir(tempDir string) string {
return tempDir + "/src"
}

func UserTempIncludeDir(tempDir string) string {
return tempDir + "/include"
}

func PkgSyscfgPath(pkgPath string) string {
return pkgPath + "/" + pkg.SYSCFG_YAML_FILENAME
}
Expand Down
27 changes: 26 additions & 1 deletion newt/builder/targetbuild.go
Original file line number Diff line number Diff line change
Expand Up @@ -330,6 +330,17 @@ func (t *TargetBuilder) PrepBuild() error {
return err
}

// Create directories where user scripts can write artifacts to incorporate
// into the build.

if err := os.MkdirAll(UserSrcDir(t.target.Name()), 0755); err != nil {
return util.NewNewtError(err.Error())
}

if err := os.MkdirAll(UserIncludeDir(t.target.Name()), 0755); err != nil {
return util.NewNewtError(err.Error())
}

var err error
if t.res.LoaderSet != nil {
t.LoaderBuilder, err = NewBuilder(t, BUILD_NAME_LOADER,
Expand Down Expand Up @@ -483,7 +494,6 @@ func (t *TargetBuilder) Build() error {
return err
}

/* Build the Apps */
project.ResetDeps(t.AppList)

if err := t.bspPkg.Reload(t.AppBuilder.cfg.SettingValues()); err != nil {
Expand All @@ -497,6 +507,16 @@ func (t *TargetBuilder) Build() error {
}
}

env, err := t.AppBuilder.EnvVars(0)
if err != nil {
return err
}

// Execute the set of pre-build user scripts.
if err := t.execPreCmds(env); err != nil {
return err
}

if err := t.AppBuilder.Build(); err != nil {
return err
}
Expand All @@ -516,6 +536,11 @@ func (t *TargetBuilder) Build() error {
return err
}

// Execute the set of post-build user scripts.
if err := t.execPostCmds(env); err != nil {
return err
}

return nil
}

Expand Down

0 comments on commit 035787e

Please sign in to comment.