You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@mynewt.apache.org by ja...@apache.org on 2026/10/20 23:01:49 UTC
(mynewt-newt) branch master updated (fb57b30c -> 2ed032d1)
This is an automated email from the ASF dual-hosted git repository.
sjanc pushed a change to branch master
in repository https://gitbox.apache.org/repos/asf/mynewt-newt.git
from fb57b30c newt/toolchain: Test syscfg conditionals in compiler.yml
new ae9932f7 newt/repo: Fix hang on version alias loop in repository.yml
new 07d10d46 newt/install: Fix panic on long answer to upgrade prompt
new 26ab97a7 newt/compat: Fix panic on duplicate version in compatibility table
new 12388a64 newt/config: Fix panic on relative import with no project loaded
new 70ed2d6f newt/project: Fix treating the project as a repo dependency
new faff17fe newt/downloader: Fix running git in project for non-git repo dirs
new 3a7a7e58 newt/parse: Fix crash on empty parentheses in expressions
new 55642e1c newt/syscfg: Reject empty pieces in range restrictions
new 96bf3cdd newt/syscfg: Fix crash on override of newt-injected settings
new 56bababf newt/syscfg: Fix crash on cmdline override of deprecated setting
new d50b2645 newt/sysdown: Fix crashes on conflicting or invalid pkg.down
new ea76f3b4 newt/builder: Fix crash for external target using local packages
new 6668f24c newt/resolve: Fix hang on non-converging syscfg
new b3e37cf3 newt/syscfg: Fix nondeterministic chained value references
new f48ae34b newt/cli: Fix target create/copy overwriting files
new 8aaefc8a newt/cli: Fix panic in man and apropos without argument
new 729d3e45 newt/cli: Fix panic in pkg copy/move with unknown source repo
new ad2da0c1 newt: Fix panic in pkg new and docs build outside of project
new 60535eaa newt/cli: Fix panic in complete with aliased program name
new a5f5663a newt/mfg: Fix panic on target without app or BSP
new 8ac3f3d9 newt/mfg: Detect overlaps of the meta region
new d0df0ce8 newt/mfg: Fix panic with meta region below base address
new 4d2e2e5b newt/extcmd: Fix panic on empty external command
new 03e277de newt/builder: Fix panic on unexpected map file memory configuration
new e053aae3 newt/builder: Fix CMakeLists.txt truncation on target cmake error
new a47b0b2a newt/toolchain: Fix data races during parallel compilation
new 26d44628 newt/toolchain: Fix compiler warnings ending up in dependency files
new d4d477cf newt/toolchain: Fix dependency files of sources with spaces in names
new 60166292 newt/toolchain: Fix builds of projects in paths with spaces
new 2ed032d1 newt/toolchain: Keep options with separate arguments together
The 30 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails. The revisions
listed as "add" were already present in the repository and have only
been added to this reference.
Summary of changes:
newt/build_errors_test.go | 102 +++++++++
newt/build_space_test.go | 104 +++++++++
newt/builder/cmake.go | 35 +--
newt/builder/extcmd.go | 4 +
newt/builder/size.go | 11 +
newt/builder/size_report.go | 7 +-
newt/builder/size_test.go | 167 ++++++++++++++
newt/builder/targetbuild.go | 31 ++-
newt/cli/complete_cmd.go | 14 +-
newt/cli/man_cmds.go | 9 +
newt/cli/pkg_cmds.go | 9 +-
newt/cli/util.go | 39 ++++
newt/cli_errors_test.go | 97 +++++++++
newt/compat/compat.go | 8 +
newt/compat/compat_test.go | 81 +++++++
newt/config/config.go | 20 +-
newt/config/config_test.go | 87 ++++++++
newt/deprepo/deprepo.go | 6 +-
newt/docs/docsbuilder.go | 7 +-
newt/downloader/downloader.go | 47 ++++
newt/dump/syscfg.go | 4 +-
newt/extcmd/extcmd.go | 13 ++
newt/install/install.go | 18 +-
newt/install/install_test.go | 68 ++++++
newt/localrepo_test.go | 82 +++++++
newt/mfg/build.go | 46 +++-
newt/mfg/build_test.go | 163 ++++++++++++++
newt/mfg_errors_test.go | 107 +++++++++
newt/parse/parse.go | 3 +
newt/parse/parse_test.go | 76 +++++++
newt/project/pkgwriter.go | 9 +-
newt/project/project.go | 9 +
newt/repo/repo.go | 3 +
newt/repo/version.go | 9 +
newt/repo/version_test.go | 119 ++++++++++
newt/repogit_test.go | 109 ++++++++++
newt/resolve/resolve.go | 105 +++++++++
newt/syscfg/restrict.go | 11 +-
newt/syscfg/restrict_test.go | 86 ++++++++
newt/syscfg/syscfg.go | 105 +++++++--
newt/syscfg/syscfg_test.go | 91 ++++++++
newt/syscfg_stability_test.go | 152 +++++++++++++
newt/sysdown/sysdown.go | 5 +-
newt/sysdown/sysdown_test.go | 89 ++++++++
newt/target_name_test.go | 133 ++++++++++++
.../local-conflict => localrepo_dep}/project.yml | 9 +-
.../success/inferrc => repogit_nogit}/project.yml | 8 +-
.../apps/app/pkg.yml | 0
.../app => syscfg_stability/apps/osc_dep}/pkg.yml | 8 +-
.../apps/osc_dep}/syscfg.yml | 3 +
.../app => syscfg_stability/apps/osc_self}/pkg.yml | 6 +-
.../apps/osc_self}/syscfg.yml | 3 +
.../sim => syscfg_stability/compiler/loc}/pkg.yml | 3 +-
.../sim => syscfg_stability/hw/bsp/loc}/bsp.yml | 4 +-
.../sim => syscfg_stability/hw/bsp/loc}/pkg.yml | 3 +-
.../project.yml | 2 +-
.../repos/apache-mynewt-core/compiler/sim/pkg.yml | 0
.../repos/apache-mynewt-core/hw/bsp/sim/bsp.yml | 0
.../repos/apache-mynewt-core/hw/bsp/sim/pkg.yml | 0
.../repos/apache-mynewt-core/repository.yml | 0
.../repos/apache-mynewt-core/sys/foo/pkg.yml | 0
.../repos/apache-mynewt-core/sys/foo/syscfg.yml | 10 +-
.../repos/apache-mynewt-core/sys/osc}/pkg.yml | 3 +-
.../repos/apache-mynewt-core/sys/osc}/syscfg.yml | 8 +-
.../repos/apache-mynewt-core/sys/oscbar}/pkg.yml | 3 +-
.../apache-mynewt-core/sys/oscbar}/syscfg.yml | 5 +-
.../repos/apache-mynewt-core/targets/ext}/pkg.yml | 3 +-
.../apache-mynewt-core/targets/ext}/target.yml | 5 +-
.../targets/deprecated}/pkg.yml | 3 +-
.../targets/deprecated}/syscfg.yml | 7 +-
.../targets/deprecated}/target.yml | 1 +
.../targets/newt_override}/pkg.yml | 3 +-
.../targets/newt_override}/syscfg.yml | 4 +-
.../targets/newt_override}/target.yml | 1 +
.../targets/osc_dep}/pkg.yml | 3 +-
.../targets/osc_dep}/target.yml | 3 +-
.../targets/osc_self}/pkg.yml | 3 +-
.../targets/osc_self}/target.yml | 3 +-
newt/toolchain/compiler.go | 127 +++++++++--
newt/toolchain/deps.go | 79 ++++++-
newt/toolchain/deps_test.go | 241 +++++++++++++++++++++
newt/toolchain/flags.go | 93 ++++++++
newt/toolchain/flags_test.go | 111 ++++++++++
83 files changed, 3135 insertions(+), 133 deletions(-)
create mode 100644 newt/build_errors_test.go
create mode 100644 newt/build_space_test.go
create mode 100644 newt/builder/size_test.go
create mode 100644 newt/cli_errors_test.go
create mode 100644 newt/compat/compat_test.go
create mode 100644 newt/config/config_test.go
create mode 100644 newt/install/install_test.go
create mode 100644 newt/localrepo_test.go
create mode 100644 newt/mfg/build_test.go
create mode 100644 newt/mfg_errors_test.go
create mode 100644 newt/parse/parse_test.go
create mode 100644 newt/repo/version_test.go
create mode 100644 newt/repogit_test.go
create mode 100644 newt/syscfg/restrict_test.go
create mode 100644 newt/syscfg/syscfg_test.go
create mode 100644 newt/syscfg_stability_test.go
create mode 100644 newt/sysdown/sysdown_test.go
create mode 100644 newt/target_name_test.go
copy newt/testdata/{upgrade/fail/local-conflict => localrepo_dep}/project.yml (85%)
copy newt/testdata/{upgrade/success/inferrc => repogit_nogit}/project.yml (85%)
copy newt/testdata/{error_reporting => syscfg_stability}/apps/app/pkg.yml (100%)
copy newt/testdata/{error_reporting/apps/app => syscfg_stability/apps/osc_dep}/pkg.yml (87%)
copy newt/testdata/{dump/proj/repos/apache-mynewt-core/compiler/arm-none-eabi-m3 => syscfg_stability/apps/osc_dep}/syscfg.yml (95%)
copy newt/testdata/{syscfg_collision/apps/app => syscfg_stability/apps/osc_self}/pkg.yml (93%)
copy newt/testdata/{dump/proj/repos/apache-mynewt-core/compiler/arm-none-eabi-m3 => syscfg_stability/apps/osc_self}/syscfg.yml (95%)
copy newt/testdata/{syscfg_collision/repos/apache-mynewt-core/compiler/sim => syscfg_stability/compiler/loc}/pkg.yml (97%)
copy newt/testdata/{error_reporting/repos/apache-mynewt-core/hw/bsp/sim => syscfg_stability/hw/bsp/loc}/bsp.yml (95%)
copy newt/testdata/{error_reporting/repos/apache-mynewt-core/hw/bsp/sim => syscfg_stability/hw/bsp/loc}/pkg.yml (97%)
copy newt/testdata/{error_reporting => syscfg_stability}/project.yml (96%)
copy newt/testdata/{error_reporting => syscfg_stability}/repos/apache-mynewt-core/compiler/sim/pkg.yml (100%)
copy newt/testdata/{error_reporting => syscfg_stability}/repos/apache-mynewt-core/hw/bsp/sim/bsp.yml (100%)
copy newt/testdata/{error_reporting => syscfg_stability}/repos/apache-mynewt-core/hw/bsp/sim/pkg.yml (100%)
copy newt/testdata/{error_reporting => syscfg_stability}/repos/apache-mynewt-core/repository.yml (100%)
copy newt/testdata/{syscfg_collision => syscfg_stability}/repos/apache-mynewt-core/sys/foo/pkg.yml (100%)
copy newt/testdata/{error_reporting => syscfg_stability}/repos/apache-mynewt-core/sys/foo/syscfg.yml (81%)
copy newt/testdata/{error_reporting/repos/apache-mynewt-core/sys/foo => syscfg_stability/repos/apache-mynewt-core/sys/osc}/pkg.yml (97%)
copy newt/testdata/{error_reporting/repos/apache-mynewt-core/sys/foo => syscfg_stability/repos/apache-mynewt-core/sys/osc}/syscfg.yml (83%)
copy newt/testdata/{error_reporting/repos/apache-mynewt-core/sys/foo => syscfg_stability/repos/apache-mynewt-core/sys/oscbar}/pkg.yml (97%)
copy newt/testdata/{dump/proj/repos/apache-mynewt-core/mgmt/newtmgr/nmgr_os => syscfg_stability/repos/apache-mynewt-core/sys/oscbar}/syscfg.yml (90%)
copy newt/testdata/{syscfg_collision/targets/fail => syscfg_stability/repos/apache-mynewt-core/targets/ext}/pkg.yml (97%)
copy newt/testdata/{error_reporting/targets/app => syscfg_stability/repos/apache-mynewt-core/targets/ext}/target.yml (90%)
copy newt/testdata/{error_reporting/targets/app => syscfg_stability/targets/deprecated}/pkg.yml (96%)
copy newt/testdata/{dump/proj/repos/apache-mynewt-core/apps/timtest => syscfg_stability/targets/deprecated}/syscfg.yml (94%)
copy newt/testdata/{syscfg_collision/targets/fail => syscfg_stability/targets/deprecated}/target.yml (99%)
copy newt/testdata/{error_reporting/targets/app => syscfg_stability/targets/newt_override}/pkg.yml (96%)
copy newt/testdata/{dump/proj/repos/apache-mynewt-core/kernel/os/test => syscfg_stability/targets/newt_override}/syscfg.yml (96%)
copy newt/testdata/{syscfg_collision/targets/fail => syscfg_stability/targets/newt_override}/target.yml (99%)
copy newt/testdata/{error_reporting/targets/app => syscfg_stability/targets/osc_dep}/pkg.yml (96%)
copy newt/testdata/{error_reporting/targets/app => syscfg_stability/targets/osc_dep}/target.yml (97%)
copy newt/testdata/{error_reporting/targets/app => syscfg_stability/targets/osc_self}/pkg.yml (96%)
copy newt/testdata/{error_reporting/targets/app => syscfg_stability/targets/osc_self}/target.yml (96%)
create mode 100644 newt/toolchain/deps_test.go
create mode 100644 newt/toolchain/flags.go
create mode 100644 newt/toolchain/flags_test.go
(mynewt-newt) 22/30: newt/mfg: Fix panic with meta region below base address
Posted by ja...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
sjanc pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/mynewt-newt.git
commit d0df0ce8425c4f7c0a8e0f5e9083b150c6e8aa68
Author: Szymon Janc <sz...@codecoup.pl>
AuthorDate: Mon Oct 19 17:53:48 2026 +0200
newt/mfg: Fix panic with meta region below base address
The meta region offset was calculated relative to the base address
given with --base-address without any checks. If the meta flash area
was located below it, e.g. in an area on another flash device at offset
0 while the image used base address 0x08000000, the negative offset
caused a slice bounds panic in mynewt-artifact when generating the
image.
Take the meta area's device into account when determining the flash
device of the image, and report an error if the meta region offset is
below the base address.
Assisted-by: Claude:claude-opus-5-5
---
newt/mfg/build.go | 8 ++++++++
newt/mfg/build_test.go | 40 ++++++++++++++++++++++++++++++++++++++++
2 files changed, 48 insertions(+)
diff --git a/newt/mfg/build.go b/newt/mfg/build.go
index 66886a27..721ef658 100644
--- a/newt/mfg/build.go
+++ b/newt/mfg/build.go
@@ -428,6 +428,9 @@ func (mb *MfgBuilder) calcDevice() (int, error) {
for _, r := range mb.Raws {
deviceMap[r.Area.Device] = struct{}{}
}
+ if mb.Meta != nil {
+ deviceMap[mb.Meta.Area.Device] = struct{}{}
+ }
devices := make([]int, 0, len(deviceMap))
for d, _ := range deviceMap {
@@ -631,6 +634,11 @@ func (mb *MfgBuilder) Build() (mfg.Mfg, error) {
}
metap = &meta
metaOff = mb.metaOffset(meta)
+ if metaOff < 0 {
+ return mfg.Mfg{}, util.FmtNewtError(
+ "meta region in flash area \"%s\" is located below "+
+ "base address 0x%x", mb.Meta.Area.Name, mb.BaseAddress)
+ }
}
return mfg.Mfg{
diff --git a/newt/mfg/build_test.go b/newt/mfg/build_test.go
index 5db104a2..ebd36284 100644
--- a/newt/mfg/build_test.go
+++ b/newt/mfg/build_test.go
@@ -121,3 +121,43 @@ func TestDetectOverlapsMeta(t *testing.T) {
t.Fatal("overlap of meta region not detected")
}
}
+
+// The meta region must be on the same flash device as the rest of the
+// image.
+func TestCalcDeviceMeta(t *testing.T) {
+ area := testAreaBoot
+ area.Device = 1
+ meta := &MfgBuildMeta{
+ Area: area,
+ Hash: true,
+ }
+
+ mb := testBuilder(meta, testRaw(t, testAreaImage, 0, 0x100))
+ if _, err := mb.calcDevice(); err == nil {
+ t.Fatal("meta region on another device not detected")
+ }
+}
+
+// The meta region must not be located below the base address.
+func TestBuildMetaBelowBase(t *testing.T) {
+ area := testAreaImage
+ area.Offset = 0x08000000
+ meta := &MfgBuildMeta{
+ Area: testAreaBoot,
+ Hash: true,
+ }
+
+ mb := testBuilder(meta, testRaw(t, area, 0, 0x100))
+ mb.BaseAddress = 0x08000000
+
+ m, err := mb.Build()
+ if err == nil {
+ defer func() {
+ if r := recover(); r != nil {
+ t.Fatalf("panic: %v", r)
+ }
+ }()
+ m.Bytes(0xff)
+ t.Fatal("meta region below base address not detected")
+ }
+}
(mynewt-newt) 19/30: newt/cli: Fix panic in complete with aliased program name
Posted by ja...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
sjanc pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/mynewt-newt.git
commit 60535eaa364d5ec451ad518e9932e1f4f62b3ac0
Author: Szymon Janc <sz...@codecoup.pl>
AuthorDate: Mon Oct 19 17:51:19 2026 +0200
newt/cli: Fix panic in complete with aliased program name
"newt complete" looked for the name of the last parsed command in
COMP_LINE. When newt was invoked under another name, e.g. through an
alias ("COMP_LINE='n ' newt complete"), the root command name "newt"
was not found, and the -1 index led to a slice bounds panic.
Skip the first word of the command line, which is the program name,
and only search for the last command name after it.
Assisted-by: Claude:claude-opus-5-5
---
newt/cli/complete_cmd.go | 14 +++++++++++---
newt/cli_errors_test.go | 25 +++++++++++++++++++++++++
2 files changed, 36 insertions(+), 3 deletions(-)
diff --git a/newt/cli/complete_cmd.go b/newt/cli/complete_cmd.go
index ae3d39e6..1b2ef51e 100644
--- a/newt/cli/complete_cmd.go
+++ b/newt/cli/complete_cmd.go
@@ -143,9 +143,17 @@ func completeRunCmd(cmd *cobra.Command, args []string) {
found_args := strings.Split(found_cmd.CommandPath(), " ")
last_arg := found_args[len(found_args)-1]
- /* what is remaining after the last parsed argument */
- ind := strings.Index(cmd_line, last_arg)
- ind += len(last_arg)
+ /* what is remaining after the last parsed argument; the first word
+ * is the program name, which may differ from the root command name
+ * (e.g., an alias or a path) */
+ ind := len(args[0])
+ if found_cmd != root_cmd {
+ i := strings.Index(cmd_line[ind:], last_arg)
+ if i < 0 {
+ return
+ }
+ ind += i + len(last_arg)
+ }
extra_str := cmd_line[ind:]
if len(extra_str) == 0 {
diff --git a/newt/cli_errors_test.go b/newt/cli_errors_test.go
index 5172af73..f64d249a 100644
--- a/newt/cli_errors_test.go
+++ b/newt/cli_errors_test.go
@@ -70,3 +70,28 @@ func TestCliNoProject(t *testing.T) {
cliErrorCheck(t, dir, nil, "No project file found", "pkg", "new", "foo")
cliErrorCheck(t, dir, nil, "No project file found", "docs", "build")
}
+
+// "newt complete" must handle command lines whose program name differs
+// from "newt", e.g., when newt is invoked through an alias.
+func TestCliCompleteAlias(t *testing.T) {
+ projDir := filepath.Join("testdata", "syscfg_collision")
+
+ for _, tc := range []struct {
+ line string
+ expected string
+ }{
+ {"n ", "newt "},
+ {"n", "newt"},
+ {"n tar", "newt tar"},
+ {"n target cr", "newt target cr"},
+ {"/usr/bin/newt target cr", "newt target cr"},
+ } {
+ got := newtOutput(t, projDir, []string{"COMP_LINE=" + tc.line},
+ "complete")
+ exp := newtOutput(t, projDir, []string{"COMP_LINE=" + tc.expected},
+ "complete")
+ if got != exp {
+ t.Errorf("COMP_LINE=%q: expected %q, got %q", tc.line, exp, got)
+ }
+ }
+}
(mynewt-newt) 15/30: newt/cli: Fix target create/copy overwriting files
Posted by ja...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
sjanc pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/mynewt-newt.git
commit f48ae34b9dcd8a10b6bb0b1b907d0373cd592291
Author: Szymon Janc <sz...@codecoup.pl>
AuthorDate: Mon Oct 19 17:46:52 2026 +0200
newt/cli: Fix target create/copy overwriting files
ResolveNewTargetName, used by "target create" and "target copy", only
checked whether a target with the given name already existed. Creating
a target named e.g. "apps/app" silently replaced the pkg.yml of an
existing non-target package, and names like "", "/", "x/.." or
"../../x" wrote pkg.yml and target.yml into the targets directory, the
project root or outside of the project.
Reject empty names and names that are not clean relative paths or
contain ".." elements, and refuse to use a destination directory that
already exists and is not empty.
Assisted-by: Claude:claude-opus-5-5
---
newt/cli/util.go | 39 ++++++++++++++
newt/target_name_test.go | 133 +++++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 172 insertions(+)
diff --git a/newt/cli/util.go b/newt/cli/util.go
index ccbb0bc4..452f3c97 100644
--- a/newt/cli/util.go
+++ b/newt/cli/util.go
@@ -24,6 +24,7 @@ import (
"bytes"
"fmt"
"os"
+ "path"
"regexp"
"strings"
@@ -151,6 +152,23 @@ func ResolveNewTargetName(name string) (string, error) {
"must be local")
}
+ if pkgName == "" {
+ return "", util.NewNewtError("Target name cannot be empty")
+ }
+
+ // Reject names that are not clean relative paths; e.g., ".", "x/..",
+ // "../x" or "/x" would place the target outside of a dedicated
+ // directory inside the project.
+ if path.IsAbs(pkgName) || path.Clean(pkgName) != pkgName ||
+ strings.Contains(pkgName, "\\") {
+ return "", util.FmtNewtError("Invalid target name: %s", name)
+ }
+ for _, elem := range strings.Split(pkgName, "/") {
+ if elem == "." || elem == ".." {
+ return "", util.FmtNewtError("Invalid target name: %s", name)
+ }
+ }
+
if pkgName == TARGET_KEYWORD_ALL {
return "", util.NewNewtError("Target name " + TARGET_KEYWORD_ALL +
" is reserved")
@@ -165,6 +183,27 @@ func ResolveNewTargetName(name string) (string, error) {
return "", util.NewNewtError("Target already exists: " + pkgName)
}
+ // The destination directory must not contain anything yet; otherwise
+ // saving the target would overwrite an existing package or files.
+ dir := TryGetProject().LocalRepo().Path() + "/" + pkgName
+ if info, err := os.Stat(dir); err == nil {
+ if !info.IsDir() {
+ return "", util.FmtNewtError(
+ "Target destination exists and is not a directory: %s",
+ pkgName)
+ }
+ entries, err := os.ReadDir(dir)
+ if err != nil {
+ return "", util.ChildNewtError(err)
+ }
+ if len(entries) > 0 {
+ return "", util.FmtNewtError(
+ "Target destination directory is not empty: %s", pkgName)
+ }
+ } else if !os.IsNotExist(err) {
+ return "", util.ChildNewtError(err)
+ }
+
return pkgName, nil
}
diff --git a/newt/target_name_test.go b/newt/target_name_test.go
new file mode 100644
index 00000000..f96cde9e
--- /dev/null
+++ b/newt/target_name_test.go
@@ -0,0 +1,133 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package main
+
+import (
+ "os"
+ "path/filepath"
+ "testing"
+)
+
+// targetNameSnapshot returns contents of all files under the grandparent
+// of dir, so that tests can verify nothing was written inside or near dir.
+func targetNameSnapshot(t *testing.T, dir string) map[string]string {
+ t.Helper()
+
+ files := map[string]string{}
+ err := filepath.Walk(filepath.Dir(filepath.Dir(dir)),
+ func(p string, info os.FileInfo, err error) error {
+ if err != nil {
+ return err
+ }
+ if !info.IsDir() {
+ files[p] = readFile(t, p)
+ }
+ return nil
+ })
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ return files
+}
+
+func targetNameCheckUnchanged(t *testing.T, dir string,
+ before map[string]string) {
+
+ t.Helper()
+
+ after := targetNameSnapshot(t, dir)
+ for p, data := range after {
+ old, ok := before[p]
+ if !ok {
+ t.Errorf("file created: %s", p)
+ } else if old != data {
+ t.Errorf("file modified: %s", p)
+ }
+ }
+ for p := range before {
+ if _, ok := after[p]; !ok {
+ t.Errorf("file removed: %s", p)
+ }
+ }
+}
+
+// Invalid or conflicting names passed to "target create" and "target copy"
+// must be rejected without touching any files.
+func TestTargetNameInvalid(t *testing.T) {
+ names := []string{
+ "",
+ "/",
+ ".",
+ "..",
+ "x/..",
+ "../../x",
+ "targets/../x",
+ "/tmp/x",
+ "apps/app",
+ "success",
+ "targets/success",
+ }
+
+ for _, name := range names {
+ for _, cmd := range [][]string{
+ {"target", "create", name},
+ {"target", "copy", "success", name},
+ } {
+ projDir := copyToTemp(t,
+ filepath.Join("testdata", "syscfg_collision"))
+ before := targetNameSnapshot(t, projDir)
+
+ out, err := newtCombinedOutput(projDir, nil, cmd...)
+ if err == nil {
+ t.Errorf("newt %q succeeded\n%s", cmd, out)
+ }
+
+ targetNameCheckUnchanged(t, projDir, before)
+ }
+ }
+}
+
+func TestTargetNameValid(t *testing.T) {
+ projDir := copyToTemp(t, filepath.Join("testdata", "syscfg_collision"))
+
+ if err := os.MkdirAll(filepath.Join(projDir, "targets", "empty"),
+ 0755); err != nil {
+ t.Fatal(err)
+ }
+
+ newtOutput(t, projDir, nil, "target", "create", "new")
+ newtOutput(t, projDir, nil, "target", "create", "apps/newtgt")
+ newtOutput(t, projDir, nil, "target", "copy", "success", "empty")
+
+ for _, p := range []string{
+ "targets/new/pkg.yml",
+ "targets/new/target.yml",
+ "apps/newtgt/pkg.yml",
+ "apps/newtgt/target.yml",
+ "targets/empty/pkg.yml",
+ "targets/empty/target.yml",
+ "targets/empty/syscfg.yml",
+ } {
+ if _, err := os.Stat(filepath.Join(projDir, p)); err != nil {
+ t.Error(err)
+ }
+ }
+}
(mynewt-newt) 08/30: newt/syscfg: Reject empty pieces in range restrictions
Posted by ja...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
sjanc pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/mynewt-newt.git
commit 55642e1c4dc9000e951eb03e4f90c547b493cb97
Author: Szymon Janc <sz...@codecoup.pl>
AuthorDate: Mon Oct 19 17:46:37 2026 +0200
newt/syscfg: Reject empty pieces in range restrictions
A range restriction with an empty piece or a missing bound, e.g.
"0..1,", "1,,3", "..5" or "", was accepted and turned into an
expression containing empty parentheses, "((X) == ())", which crashed
newt when evaluated.
Reject empty range pieces and missing lower/upper bounds with an error
naming the setting and the full range expression.
Assisted-by: Claude:claude-opus-5-5
---
newt/syscfg/restrict.go | 11 ++++--
newt/syscfg/restrict_test.go | 86 ++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 94 insertions(+), 3 deletions(-)
diff --git a/newt/syscfg/restrict.go b/newt/syscfg/restrict.go
index 6c70498a..84e92cc0 100644
--- a/newt/syscfg/restrict.go
+++ b/newt/syscfg/restrict.go
@@ -359,13 +359,18 @@ func createRangeRestriction(baseSetting string, expr string) (CfgRestriction, er
rtoken := CfgRestrictionRange{}
limits := strings.Split(token, "..")
- if len(limits) == 1 {
+ for i := range limits {
+ limits[i] = strings.TrimSpace(limits[i])
+ }
+ if len(limits) == 1 && limits[0] != "" {
rtoken.LExpr = limits[0]
- } else if len(limits) == 2 && len(strings.TrimSpace(limits[1])) > 0 {
+ } else if len(limits) == 2 && limits[0] != "" && limits[1] != "" {
rtoken.LExpr = limits[0]
rtoken.RExpr = limits[1]
} else {
- return r, util.FmtNewtError("invalid token in range expression \"%s\"", token)
+ return r, util.FmtNewtError(
+ "invalid token \"%s\" in range expression \"%s\" "+
+ "for setting %s", token, expr, baseSetting)
}
r.Ranges = append(r.Ranges, rtoken)
diff --git a/newt/syscfg/restrict_test.go b/newt/syscfg/restrict_test.go
new file mode 100644
index 00000000..502ef9c4
--- /dev/null
+++ b/newt/syscfg/restrict_test.go
@@ -0,0 +1,86 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package syscfg
+
+import (
+ "strings"
+ "testing"
+
+ "mynewt.apache.org/newt/newt/cfgv"
+)
+
+func TestCreateRangeRestrictionInvalid(t *testing.T) {
+ exprs := []string{
+ "",
+ " ",
+ "0..1,",
+ ",0..1",
+ "1,,3",
+ "..5",
+ "5..",
+ "1..2..3",
+ "1.. ,3",
+ }
+ for _, expr := range exprs {
+ _, err := createRangeRestriction("FOO", expr)
+ if err == nil {
+ t.Errorf("expected error for range \"%s\"", expr)
+ continue
+ }
+ if !strings.Contains(err.Error(), "FOO") {
+ t.Errorf("error for range \"%s\" does not name setting: %s",
+ expr, err.Error())
+ }
+ }
+}
+
+func TestCreateRangeRestrictionValid(t *testing.T) {
+ tests := []struct {
+ expr string
+ val string
+ met bool
+ }{
+ {"0..10", "5", true},
+ {"0..10", "11", false},
+ {"0..1, 5", "5", true},
+ {" 0 .. 1 , 5 ", "4", false},
+ {"1,2,3", "2", true},
+ {"MIN..MAX", "7", true},
+ }
+ for _, tc := range tests {
+ r, err := createRangeRestriction("FOO", tc.expr)
+ if err != nil {
+ t.Errorf("unexpected error for range \"%s\": %v", tc.expr, err)
+ continue
+ }
+
+ cfg := NewCfg()
+ cfg.Settings["FOO"] = CfgEntry{Name: "FOO", Value: tc.val}
+ settings := cfgv.NewSettingsFromMap(map[string]string{
+ "FOO": tc.val,
+ "MIN": "3",
+ "MAX": "8",
+ })
+ if met := cfg.restrictionMet(r, settings); met != tc.met {
+ t.Errorf("range \"%s\" value %s: got %v, want %v",
+ tc.expr, tc.val, met, tc.met)
+ }
+ }
+}
(mynewt-newt) 12/30: newt/builder: Fix crash for external target using local packages
Posted by ja...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
sjanc pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/mynewt-newt.git
commit ea76f3b42df1fbbeaeea3935a1e9fb2b66abdba4
Author: Szymon Janc <sz...@codecoup.pl>
AuthorDate: Mon Oct 19 17:50:50 2026 +0200
newt/builder: Fix crash for external target using local packages
resolveTransientPkgs re-resolved each seed package by its full name
relative to the target's repo. The full name of a local project package
has no "@repo" prefix, so for a target located in an external repo
that uses e.g. a BSP or compiler from the local project, the lookup
failed and a nil package was put into the seed list. Resolving
dependencies then crashed with a nil pointer dereference.
The seed packages are already resolved, so only follow the link of
transient packages, relative to the transient package's own repo, and
report an error if the linked package does not exist.
Assisted-by: Claude:claude-opus-5-5
---
newt/builder/targetbuild.go | 31 +++++++++++++++++-----
newt/syscfg_stability_test.go | 17 ++++++++++++
.../testdata/syscfg_stability/compiler/loc/pkg.yml | 22 +++++++++++++++
newt/testdata/syscfg_stability/hw/bsp/loc/bsp.yml | 28 +++++++++++++++++++
newt/testdata/syscfg_stability/hw/bsp/loc/pkg.yml | 22 +++++++++++++++
.../repos/apache-mynewt-core/targets/ext/pkg.yml | 22 +++++++++++++++
.../apache-mynewt-core/targets/ext/target.yml | 23 ++++++++++++++++
7 files changed, 158 insertions(+), 7 deletions(-)
diff --git a/newt/builder/targetbuild.go b/newt/builder/targetbuild.go
index e8937192..b68ddbeb 100644
--- a/newt/builder/targetbuild.go
+++ b/newt/builder/targetbuild.go
@@ -37,6 +37,7 @@ import (
"mynewt.apache.org/newt/newt/interfaces"
"mynewt.apache.org/newt/newt/pkg"
"mynewt.apache.org/newt/newt/project"
+ "mynewt.apache.org/newt/newt/repo"
"mynewt.apache.org/newt/newt/resolve"
"mynewt.apache.org/newt/newt/symbol"
"mynewt.apache.org/newt/newt/syscfg"
@@ -166,14 +167,26 @@ func (t *TargetBuilder) injectBuildSettings() {
// resolveTransientPkgs replaces packages in a slice with the packages they
// link to. It has no effect on non-transient packages.
-func (t *TargetBuilder) resolveTransientPkgs(lps []*pkg.LocalPackage) {
+func (t *TargetBuilder) resolveTransientPkgs(lps []*pkg.LocalPackage) error {
for i, lp := range lps {
- resolved := t.target.ResolvePackageName(lp.FullName())
- if resolved != lp {
- resolve.LogTransientWarning(lp)
- lps[i] = resolved
+ if lp.Type() != pkg.PACKAGE_TYPE_TRANSIENT {
+ continue
}
+
+ // The link is relative to the transient package's own repo.
+ resolved := t.target.ResolvePackageRepoAndName(
+ lp.Repo().(*repo.Repo), lp.LinkedName())
+ if resolved == nil {
+ return util.FmtNewtError(
+ "transient package %s links to unknown package %s",
+ lp.FullName(), lp.LinkedName())
+ }
+
+ resolve.LogTransientWarning(lp)
+ lps[i] = resolved
}
+
+ return nil
}
func (t *TargetBuilder) ensureResolved(detectErr bool) error {
@@ -197,7 +210,9 @@ func (t *TargetBuilder) ensureResolved(detectErr bool) error {
t.compilerPkg,
t.target.Package(),
}
- t.resolveTransientPkgs(loaderSeeds)
+ if err := t.resolveTransientPkgs(loaderSeeds); err != nil {
+ return err
+ }
// For split images, inject the SPLIT_[...] settings into the
// corresponding app packages. This ensures that:
@@ -220,7 +235,9 @@ func (t *TargetBuilder) ensureResolved(detectErr bool) error {
t.compilerPkg,
t.target.Package(),
}
- t.resolveTransientPkgs(appSeeds)
+ if err := t.resolveTransientPkgs(appSeeds); err != nil {
+ return err
+ }
if t.appPkg != nil {
appSeeds = append(appSeeds, t.target.AppYml())
diff --git a/newt/syscfg_stability_test.go b/newt/syscfg_stability_test.go
index 5b03e6e7..59f5e57b 100644
--- a/newt/syscfg_stability_test.go
+++ b/newt/syscfg_stability_test.go
@@ -69,3 +69,20 @@ func TestSyscfgStabilityCmdlineDeprecated(t *testing.T) {
}
}
}
+
+// A target inside an external repo can use a BSP and compiler from the
+// local project.
+func TestSyscfgStabilityExtRepoTarget(t *testing.T) {
+ out, err := newtCombinedOutput(syscfgStabilityDir, nil, "target", "dep",
+ "@apache-mynewt-core/targets/ext")
+ syscfgStabilityCheckNoPanic(t, out)
+ if err != nil {
+ t.Fatalf("newt target dep failed: %s\n%s", err, out)
+ }
+
+ for _, p := range []string{"hw/bsp/loc", "compiler/loc"} {
+ if !strings.Contains(out, "* "+p+" -->") {
+ t.Errorf("missing package %s in dependency graph\n%s", p, out)
+ }
+ }
+}
diff --git a/newt/testdata/syscfg_stability/compiler/loc/pkg.yml b/newt/testdata/syscfg_stability/compiler/loc/pkg.yml
new file mode 100644
index 00000000..e174d605
--- /dev/null
+++ b/newt/testdata/syscfg_stability/compiler/loc/pkg.yml
@@ -0,0 +1,22 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+#
+
+
+pkg.name: compiler/loc
+pkg.type: compiler
diff --git a/newt/testdata/syscfg_stability/hw/bsp/loc/bsp.yml b/newt/testdata/syscfg_stability/hw/bsp/loc/bsp.yml
new file mode 100644
index 00000000..7d7dafc3
--- /dev/null
+++ b/newt/testdata/syscfg_stability/hw/bsp/loc/bsp.yml
@@ -0,0 +1,28 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+#
+
+
+bsp.arch: sim
+bsp.compiler: "@syscfg_stability/compiler/loc"
+bsp.flash_map:
+ areas:
+ FLASH_AREA_IMAGE_0:
+ device: 0
+ offset: 0x00000000
+ size: 64kB
diff --git a/newt/testdata/syscfg_stability/hw/bsp/loc/pkg.yml b/newt/testdata/syscfg_stability/hw/bsp/loc/pkg.yml
new file mode 100644
index 00000000..a1516526
--- /dev/null
+++ b/newt/testdata/syscfg_stability/hw/bsp/loc/pkg.yml
@@ -0,0 +1,22 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+#
+
+
+pkg.name: hw/bsp/loc
+pkg.type: bsp
diff --git a/newt/testdata/syscfg_stability/repos/apache-mynewt-core/targets/ext/pkg.yml b/newt/testdata/syscfg_stability/repos/apache-mynewt-core/targets/ext/pkg.yml
new file mode 100644
index 00000000..ad244bbe
--- /dev/null
+++ b/newt/testdata/syscfg_stability/repos/apache-mynewt-core/targets/ext/pkg.yml
@@ -0,0 +1,22 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+#
+
+
+pkg.name: targets/ext
+pkg.type: target
diff --git a/newt/testdata/syscfg_stability/repos/apache-mynewt-core/targets/ext/target.yml b/newt/testdata/syscfg_stability/repos/apache-mynewt-core/targets/ext/target.yml
new file mode 100644
index 00000000..58460d33
--- /dev/null
+++ b/newt/testdata/syscfg_stability/repos/apache-mynewt-core/targets/ext/target.yml
@@ -0,0 +1,23 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+#
+
+
+target.app: "@syscfg_stability/apps/app"
+target.bsp: "@syscfg_stability/hw/bsp/loc"
+target.build_profile: default
(mynewt-newt) 25/30: newt/builder: Fix CMakeLists.txt truncation on target cmake error
Posted by ja...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
sjanc pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/mynewt-newt.git
commit e053aae3b9b3f34b0f8db8c0d6c1aa3bc0e36bc2
Author: Szymon Janc <sz...@codecoup.pl>
AuthorDate: Mon Oct 19 17:47:38 2026 +0200
newt/builder: Fix CMakeLists.txt truncation on target cmake error
newt target cmake created CMakeLists.txt before resolving and
preparing the target, so any error (e.g. a syscfg conflict) left an
empty file behind, destroying the previous content.
The output is already generated into a buffer; write the file only
after generation succeeded and report write errors.
Assisted-by: Claude:claude-opus-5-5
---
newt/build_errors_test.go | 18 ++++++++++++++++++
newt/builder/cmake.go | 13 ++++++-------
2 files changed, 24 insertions(+), 7 deletions(-)
diff --git a/newt/build_errors_test.go b/newt/build_errors_test.go
index eead14a1..0cfa4467 100644
--- a/newt/build_errors_test.go
+++ b/newt/build_errors_test.go
@@ -82,3 +82,21 @@ func TestBuildEmptyExtCmd(t *testing.T) {
})
}
}
+
+func TestBuildCmakeErrorKeepsFile(t *testing.T) {
+ dir := buildErrProject(t)
+ path := filepath.Join(dir, "CMakeLists.txt")
+ content := "# user content\n"
+ if err := os.WriteFile(path, []byte(content), 0644); err != nil {
+ t.Fatal(err)
+ }
+
+ out, err := newtCombinedOutput(dir, nil, "target", "cmake", "fail")
+ if err == nil {
+ t.Fatalf("newt target cmake succeeded unexpectedly:\n%s", out)
+ }
+
+ if got := readFile(t, path); got != content {
+ t.Fatalf("CMakeLists.txt modified on error; got:\n%q", got)
+ }
+}
diff --git a/newt/builder/cmake.go b/newt/builder/cmake.go
index 45381d57..6e5a37b3 100644
--- a/newt/builder/cmake.go
+++ b/newt/builder/cmake.go
@@ -428,14 +428,8 @@ func CmakeHeaderWrite(w io.Writer, c *toolchain.Compiler, projectName string) {
}
func CMakeTargetGenerate(target *target.Target) error {
- CmakeFileHandle, err := os.Create(CmakeListsPath())
- if err != nil {
- return util.ChildNewtError(err)
- }
-
var b = bytes.Buffer{}
w := bufio.NewWriter(&b)
- defer CmakeFileHandle.Close()
targetBuilder, err := NewTargetBuilder(target)
if err != nil {
@@ -459,6 +453,11 @@ func CMakeTargetGenerate(target *target.Target) error {
w.Flush()
- CmakeFileHandle.Write(b.Bytes())
+ // Only write the file once generation succeeded so that an error does
+ // not destroy an existing CMakeLists.txt.
+ if err := os.WriteFile(CmakeListsPath(), b.Bytes(), 0644); err != nil {
+ return util.ChildNewtError(err)
+ }
+
return nil
}
(mynewt-newt) 01/30: newt/repo: Fix hang on version alias loop in repository.yml
Posted by ja...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
sjanc pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/mynewt-newt.git
commit ae9932f75325a7341d836885b5017c43c245e68c
Author: Szymon Janc <sz...@codecoup.pl>
AuthorDate: Mon Oct 19 17:45:59 2026 +0200
newt/repo: Fix hang on version alias loop in repository.yml
Version aliases in repository.yml (e.g. "1-latest": "1-dev") were
followed without remembering visited versions, so a loop such as
"1-latest": "1-dev" with "1-dev": "1-latest", or an alias mapping to
itself, made NormalizeVersion spin forever.
Track visited versions and report an error naming the repo and the
version where the loop was detected.
Assisted-by: Claude:claude-opus-5-5
---
newt/repo/version.go | 9 ++++
newt/repo/version_test.go | 119 ++++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 128 insertions(+)
diff --git a/newt/repo/version.go b/newt/repo/version.go
index 8329e9da..3f0830cf 100644
--- a/newt/repo/version.go
+++ b/newt/repo/version.go
@@ -272,12 +272,21 @@ func (r *Repo) NormalizeVersion(
ver newtutil.RepoVersion) (newtutil.RepoVersion, error) {
origVer := ver
+ visited := map[newtutil.RepoVersion]struct{}{}
for {
if ver.Stability == "" ||
ver.Stability == newtutil.VERSION_STABILITY_NONE {
return ver, nil
}
+ if _, ok := visited[ver]; ok {
+ return origVer, util.FmtNewtError(
+ "cannot normalize version \"%s\" for repo \"%s\"; "+
+ "version alias loop at \"%s\"",
+ origVer.String(), r.Name(), ver.String())
+ }
+ visited[ver] = struct{}{}
+
verStr := r.vers[ver]
if verStr == "" {
return ver, util.FmtNewtError(
diff --git a/newt/repo/version_test.go b/newt/repo/version_test.go
new file mode 100644
index 00000000..1257e2bd
--- /dev/null
+++ b/newt/repo/version_test.go
@@ -0,0 +1,119 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package repo
+
+import (
+ "strings"
+ "testing"
+ "time"
+
+ "mynewt.apache.org/newt/newt/newtutil"
+)
+
+func mustParseVer(t *testing.T, s string) newtutil.RepoVersion {
+ t.Helper()
+ v, err := newtutil.ParseRepoVersion(s)
+ if err != nil {
+ t.Fatalf("failed to parse version %q: %v", s, err)
+ }
+ return v
+}
+
+func normalizeWithTimeout(t *testing.T, r *Repo,
+ ver newtutil.RepoVersion) (newtutil.RepoVersion, error) {
+
+ t.Helper()
+
+ type result struct {
+ ver newtutil.RepoVersion
+ err error
+ }
+ ch := make(chan result, 1)
+ go func() {
+ v, err := r.NormalizeVersion(ver)
+ ch <- result{v, err}
+ }()
+
+ select {
+ case res := <-ch:
+ return res.ver, res.err
+ case <-time.After(5 * time.Second):
+ t.Fatalf("NormalizeVersion(%s) did not return (alias loop?)",
+ ver.String())
+ return ver, nil
+ }
+}
+
+func TestNormalizeVersion(t *testing.T) {
+ r := &Repo{
+ name: "dummy",
+ vers: map[newtutil.RepoVersion]string{
+ mustParseVer(t, "1.2.3"): "v1.2.3",
+ mustParseVer(t, "1-latest"): "1.2.3",
+ mustParseVer(t, "1-dev"): "1-latest",
+ },
+ }
+
+ v, err := normalizeWithTimeout(t, r, mustParseVer(t, "1-dev"))
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ if v != mustParseVer(t, "1.2.3") {
+ t.Fatalf("unexpected version: %s", v.String())
+ }
+}
+
+func TestNormalizeVersionAliasLoop(t *testing.T) {
+ tests := map[string]map[string]string{
+ "self": {
+ "1-dev": "1-dev",
+ },
+ "two": {
+ "1-latest": "1-dev",
+ "1-dev": "1-latest",
+ },
+ "three": {
+ "1-latest": "1-dev",
+ "1-dev": "1-stable",
+ "1-stable": "1-latest",
+ },
+ }
+
+ for name, vers := range tests {
+ t.Run(name, func(t *testing.T) {
+ r := &Repo{
+ name: "dummy",
+ vers: map[newtutil.RepoVersion]string{},
+ }
+ for k, v := range vers {
+ r.vers[mustParseVer(t, k)] = v
+ }
+
+ _, err := normalizeWithTimeout(t, r, mustParseVer(t, "1-dev"))
+ if err == nil {
+ t.Fatalf("expected error for version alias loop")
+ }
+ if !strings.Contains(err.Error(), "dummy") ||
+ !strings.Contains(err.Error(), "loop") {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ })
+ }
+}
(mynewt-newt) 24/30: newt/builder: Fix panic on unexpected map file memory configuration
Posted by ja...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
sjanc pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/mynewt-newt.git
commit 03e277de6898297c756f9772945d6f6dce3d3fc9
Author: Szymon Janc <sz...@codecoup.pl>
AuthorDate: Mon Oct 19 17:47:17 2026 +0200
newt/builder: Fix panic on unexpected map file memory configuration
newt size parsed the "Memory Configuration" section of the linker map
file assuming each line up to "*default*" has at least three fields.
A map file without the "*default*" line (blank line follows the
memory regions) or with a truncated line caused an index out of range
panic in both the size and size report code.
Skip blank lines, stop at "Linker script and memory map" and check the
number of fields before using them.
Assisted-by: Claude:claude-opus-5-5
---
newt/builder/size.go | 11 +++
newt/builder/size_report.go | 7 +-
newt/builder/size_test.go | 167 ++++++++++++++++++++++++++++++++++++++++++++
3 files changed, 184 insertions(+), 1 deletion(-)
diff --git a/newt/builder/size.go b/newt/builder/size.go
index 37841634..04debad2 100644
--- a/newt/builder/size.go
+++ b/newt/builder/size.go
@@ -193,7 +193,18 @@ func ParseMapFileSizes(fileName string) (map[string]*PkgSize, error) {
state = 3
continue
}
+ if strings.Contains(scanner.Text(),
+ "Linker script and memory map") {
+ state = 4
+ continue
+ }
array := strings.Fields(scanner.Text())
+ if len(array) == 0 {
+ continue
+ }
+ if len(array) < 3 {
+ return nil, util.NewNewtError("Can't parse mem info")
+ }
offset, err := strconv.ParseUint(array[1], 0, 64)
if err != nil {
return nil, util.NewNewtError("Can't parse mem info")
diff --git a/newt/builder/size_report.go b/newt/builder/size_report.go
index 5f7d590a..d0cc801e 100644
--- a/newt/builder/size_report.go
+++ b/newt/builder/size_report.go
@@ -264,11 +264,16 @@ func parseMapFileRegions(fileName string, sectionName string) (*MemoryRegion,
state = 2
}
case 2:
- if strings.Contains(scanner.Text(), "*default*") {
+ if strings.Contains(scanner.Text(), "*default*") ||
+ strings.Contains(scanner.Text(),
+ "Linker script and memory map") {
state = 3
continue
}
array := strings.Fields(scanner.Text())
+ if len(array) < 3 {
+ continue
+ }
offset, err := strconv.ParseUint(array[1], 0, 64)
if err != nil {
continue
diff --git a/newt/builder/size_test.go b/newt/builder/size_test.go
new file mode 100644
index 00000000..206c752f
--- /dev/null
+++ b/newt/builder/size_test.go
@@ -0,0 +1,167 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package builder
+
+import (
+ "os"
+ "path/filepath"
+ "testing"
+ "time"
+)
+
+const sizeTestMapNoDefault = `Archive member included to satisfy reference
+
+Memory Configuration
+
+Name Origin Length Attributes
+FLASH 0x0000000000008000 0x0000000000040000 xr
+
+RAM 0x0000000020000000 0x0000000000010000 xrw
+
+Linker script and memory map
+
+ .text 0x0000000000008000 0x10 obj/foo.o
+`
+
+const sizeTestMapShortLine = `Memory Configuration
+
+Name Origin Length Attributes
+FLASH 0x0000000000008000
+*default* 0x0000000000000000 0xffffffffffffffff
+
+Linker script and memory map
+`
+
+const sizeTestMapNoMemory = `Memory Configuration
+
+Name Origin Length Attributes
+
+Linker script and memory map
+
+ .text 0x0000000000008000 0x10 obj/foo.o
+`
+
+func sizeTestWriteMap(t *testing.T, content string) string {
+ t.Helper()
+
+ path := filepath.Join(t.TempDir(), "test.elf.map")
+ if err := os.WriteFile(path, []byte(content), 0644); err != nil {
+ t.Fatal(err)
+ }
+
+ return path
+}
+
+// sizeTestRun runs f and fails the test if it panics or does not finish
+// within a few seconds.
+func sizeTestRun(t *testing.T, f func()) {
+ t.Helper()
+
+ done := make(chan interface{})
+ go func() {
+ defer func() {
+ done <- recover()
+ }()
+ f()
+ }()
+
+ select {
+ case p := <-done:
+ if p != nil {
+ t.Fatalf("panic: %v", p)
+ }
+ case <-time.After(10 * time.Second):
+ t.Fatal("timeout")
+ }
+}
+
+func TestParseMapFileSizesMalformed(t *testing.T) {
+ for name, content := range map[string]string{
+ "no_default": sizeTestMapNoDefault,
+ "no_memory": sizeTestMapNoMemory,
+ } {
+ t.Run(name, func(t *testing.T) {
+ path := sizeTestWriteMap(t, content)
+ sizeTestRun(t, func() {
+ if _, err := ParseMapFileSizes(path); err != nil {
+ t.Errorf("unexpected error: %s", err)
+ }
+ })
+ })
+ }
+
+ t.Run("short_line", func(t *testing.T) {
+ path := sizeTestWriteMap(t, sizeTestMapShortLine)
+ sizeTestRun(t, func() {
+ if _, err := ParseMapFileSizes(path); err == nil {
+ t.Errorf("expected error")
+ }
+ })
+ })
+}
+
+func TestParseMapFileSizesNoDefault(t *testing.T) {
+ path := sizeTestWriteMap(t, sizeTestMapNoDefault)
+ sizeTestRun(t, func() {
+ pkgs, err := ParseMapFileSizes(path)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if len(globalMemSections) != 2 ||
+ globalMemSections["FLASH"] == nil ||
+ globalMemSections["RAM"] == nil {
+
+ t.Errorf("unexpected memory sections: %v", globalMemSections)
+ }
+ if pkgs["obj/foo.o"] == nil {
+ t.Errorf("symbols not parsed: %v", pkgs)
+ }
+ })
+}
+
+func TestParseMapFileRegionsMalformed(t *testing.T) {
+ for name, content := range map[string]string{
+ "no_default": sizeTestMapNoDefault,
+ "short_line": sizeTestMapShortLine,
+ "no_memory": sizeTestMapNoMemory,
+ } {
+ t.Run(name, func(t *testing.T) {
+ path := sizeTestWriteMap(t, content)
+ sizeTestRun(t, func() {
+ if _, err := parseMapFileRegions(path, "FLASH"); err != nil {
+ t.Errorf("unexpected error: %s", err)
+ }
+ })
+ })
+ }
+
+ path := sizeTestWriteMap(t, sizeTestMapNoDefault)
+ sizeTestRun(t, func() {
+ r, err := parseMapFileRegions(path, "RAM")
+ if err != nil {
+ t.Fatal(err)
+ }
+ if r.Name != "RAM" || r.Offset != 0x20000000 ||
+ r.EndOff != 0x20010000 {
+
+ t.Errorf("unexpected region: %+v", r)
+ }
+ })
+}
(mynewt-newt) 21/30: newt/mfg: Detect overlaps of the meta region
Posted by ja...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
sjanc pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/mynewt-newt.git
commit 8ac3f3d9c8ca4ea7246e51648f9d61cbbdd1a867
Author: Szymon Janc <sz...@codecoup.pl>
AuthorDate: Mon Oct 19 17:53:27 2026 +0200
newt/mfg: Detect overlaps of the meta region
The mfg meta region is written at the end of its flash area on top of
the image contents, but detectOverlaps() only checked raw and target
parts against each other. A meta region overlapping a part silently
overwrote the last bytes of that part.
Include the meta region in overlap detection.
Assisted-by: Claude:claude-opus-5-5
---
newt/mfg/build.go | 24 +++++++++-
newt/mfg/build_test.go | 123 +++++++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 146 insertions(+), 1 deletion(-)
diff --git a/newt/mfg/build.go b/newt/mfg/build.go
index 648dc0d5..66886a27 100644
--- a/newt/mfg/build.go
+++ b/newt/mfg/build.go
@@ -372,6 +372,21 @@ func (mb *MfgBuilder) detectOverlaps() error {
return err
}
+ // The meta region is written over the image contents, so it must not
+ // overlap any other part either.
+ if mb.Meta != nil {
+ meta, err := mb.buildMeta()
+ if err != nil {
+ return err
+ }
+ parts = append(parts, Part{
+ Name: fmt.Sprintf("meta (%s)", mb.Meta.Area.Name),
+ Offset: mb.metaOffset(meta),
+ Data: make([]byte, meta.Size()),
+ })
+ parts = SortParts(parts)
+ }
+
for i, p1 := range parts[:len(parts)-1] {
p1end := p1.Offset + len(p1.Data)
for _, p2 := range parts[i+1:] {
@@ -588,6 +603,13 @@ func (mb *MfgBuilder) buildMeta() (mfg.Meta, error) {
return meta, nil
}
+// Calculates the offset of the meta region within the manufacturing image.
+// The meta region is located at the end of its flash area.
+func (mb *MfgBuilder) metaOffset(meta mfg.Meta) int {
+ return mb.Meta.Area.Offset + mb.Meta.Area.Size - meta.Size() -
+ mb.BaseAddress
+}
+
// Builds a manufacturing image.
func (mb *MfgBuilder) Build() (mfg.Mfg, error) {
parts, err := mb.parts()
@@ -608,7 +630,7 @@ func (mb *MfgBuilder) Build() (mfg.Mfg, error) {
return mfg.Mfg{}, err
}
metap = &meta
- metaOff = mb.Meta.Area.Offset + mb.Meta.Area.Size - meta.Size() - mb.BaseAddress
+ metaOff = mb.metaOffset(meta)
}
return mfg.Mfg{
diff --git a/newt/mfg/build_test.go b/newt/mfg/build_test.go
new file mode 100644
index 00000000..5db104a2
--- /dev/null
+++ b/newt/mfg/build_test.go
@@ -0,0 +1,123 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package mfg
+
+import (
+ "os"
+ "path/filepath"
+ "strings"
+ "testing"
+
+ "github.com/apache/mynewt-artifact/flash"
+ "mynewt.apache.org/newt/newt/flashmap"
+ "mynewt.apache.org/newt/newt/pkg"
+)
+
+var testAreaBoot = flash.FlashArea{
+ Name: "FLASH_AREA_BOOTLOADER",
+ Id: 0,
+ Device: 0,
+ Offset: 0x0,
+ Size: 0x4000,
+}
+
+var testAreaImage = flash.FlashArea{
+ Name: "FLASH_AREA_IMAGE_0",
+ Id: 1,
+ Device: 0,
+ Offset: 0x4000,
+ Size: 0x8000,
+}
+
+// testRaw creates a raw entry with a file of the given size placed in the
+// given area.
+func testRaw(t *testing.T, area flash.FlashArea, offset int,
+ size int) MfgBuildRaw {
+
+ t.Helper()
+
+ path := filepath.Join(t.TempDir(), "raw.bin")
+ if err := os.WriteFile(path, make([]byte, size), 0644); err != nil {
+ t.Fatal(err)
+ }
+
+ return MfgBuildRaw{
+ Filename: path,
+ Offset: offset,
+ Size: size,
+ Area: area,
+ }
+}
+
+func testBuilder(meta *MfgBuildMeta, raws ...MfgBuildRaw) MfgBuilder {
+ fm := flashmap.FlashMap{
+ Areas: map[string]flash.FlashArea{},
+ }
+ for _, r := range raws {
+ fm.Areas[r.Area.Name] = r.Area
+ }
+ if meta != nil {
+ fm.Areas[meta.Area.Name] = meta.Area
+ }
+
+ return MfgBuilder{
+ Bsp: &pkg.BspPackage{FlashMap: fm},
+ Raws: raws,
+ Meta: meta,
+ }
+}
+
+// The meta region is located at the end of its area and must not overlap
+// any part of the image.
+func TestDetectOverlapsMeta(t *testing.T) {
+ meta := &MfgBuildMeta{
+ Area: testAreaBoot,
+ Hash: true,
+ FlashMap: true,
+ }
+
+ // Raw part followed by the meta region in the same area.
+ mb := testBuilder(meta, testRaw(t, testAreaBoot, 0, 0x100),
+ testRaw(t, testAreaImage, 0, testAreaImage.Size))
+ if err := mb.detectOverlaps(); err != nil {
+ t.Errorf("unexpected error: %s", err)
+ }
+
+ // Raw part filling the whole area of the meta region.
+ mb = testBuilder(meta, testRaw(t, testAreaBoot, 0, testAreaBoot.Size))
+ err := mb.detectOverlaps()
+ if err == nil {
+ t.Fatal("overlap of meta region not detected")
+ }
+ if !strings.Contains(err.Error(), "meta (FLASH_AREA_BOOTLOADER)") {
+ t.Errorf("unexpected error: %s", err)
+ }
+
+ // Meta region in an area which is fully used by a raw part.
+ meta = &MfgBuildMeta{
+ Area: testAreaImage,
+ Hash: true,
+ }
+ mb = testBuilder(meta, testRaw(t, testAreaBoot, 0, 0x100),
+ testRaw(t, testAreaImage, 0, testAreaImage.Size))
+ if err := mb.detectOverlaps(); err == nil {
+ t.Fatal("overlap of meta region not detected")
+ }
+}
(mynewt-newt) 05/30: newt/project: Fix treating the project as a repo dependency
Posted by ja...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
sjanc pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/mynewt-newt.git
commit 70ed2d6f9984b17a8d5357beb51fb6d9b37e232b
Author: Szymon Janc <sz...@codecoup.pl>
AuthorDate: Mon Oct 19 17:49:29 2026 +0200
newt/project: Fix treating the project as a repo dependency
The local project repo is stored in the repo map under project.name.
If a repo depends on a repo with the same name as the project (e.g.
apache-mynewt-core depends on mcuboot and the project is an mcuboot
checkout), newt took the local repo for that dependency, "updated" it
and ran "git remote set-url origin ''" on the project checkout. The
upgrade then failed normalizing the dependency version on the local
repo.
A dependency on the project's name is now considered satisfied by the
project itself: it is not loaded, updated or installed, and it is
left out of the repo dependency graph.
Assisted-by: Claude:claude-opus-5-5
---
newt/deprepo/deprepo.go | 6 ++-
newt/install/install.go | 5 +-
newt/localrepo_test.go | 82 +++++++++++++++++++++++++++++++++
newt/project/project.go | 9 ++++
newt/testdata/localrepo_dep/project.yml | 27 +++++++++++
5 files changed, 125 insertions(+), 4 deletions(-)
diff --git a/newt/deprepo/deprepo.go b/newt/deprepo/deprepo.go
index 6bbc7ac8..a371d877 100644
--- a/newt/deprepo/deprepo.go
+++ b/newt/deprepo/deprepo.go
@@ -136,8 +136,10 @@ func BuildDepGraph(repos RepoMap, rootReqs RequirementMap) (DepGraph, error) {
reqMap := RequirementMap{}
for _, d := range deps {
depRepo := repos[d.Name]
- // This might be nil if d.Name is not on allowed repositories list
- if depRepo == nil {
+ // This might be nil if d.Name is not on allowed repositories
+ // list. A dependency on the local project repo (same name
+ // as the project) is satisfied by the project itself.
+ if depRepo == nil || depRepo.IsLocal() {
continue
}
verReqs, err := depRepo.NormalizeVerReq(d.VerReqs)
diff --git a/newt/install/install.go b/newt/install/install.go
index 2d48eea5..a3dc53f3 100644
--- a/newt/install/install.go
+++ b/newt/install/install.go
@@ -200,8 +200,9 @@ func (inst *Installer) ensureDepsInList(repos []*repo.Repo,
}
for _, d := range deps {
depRepo := inst.repos[d.Name]
- // This might be nil if d.Name is not on allowed repositories list
- if depRepo == nil {
+ // This might be nil if d.Name is not on allowed repositories
+ // list. The local project repo is never installed.
+ if depRepo == nil || depRepo.IsLocal() {
continue
}
result = append(result, recurse(depRepo)...)
diff --git a/newt/localrepo_test.go b/newt/localrepo_test.go
new file mode 100644
index 00000000..ed2fc15c
--- /dev/null
+++ b/newt/localrepo_test.go
@@ -0,0 +1,82 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package main
+
+import (
+ "path/filepath"
+ "strings"
+ "testing"
+)
+
+var localRepoDir = filepath.Join("testdata", "localrepo_dep")
+
+// localRepoGitInit turns dir into a git repository with a single commit and
+// an "origin" remote. Returns the commit hash.
+func localRepoGitInit(t *testing.T, env []string, dir string,
+ origin string) string {
+
+ t.Helper()
+
+ env = append(env,
+ "GIT_AUTHOR_NAME=newt test", "GIT_AUTHOR_EMAIL=test@dummy.invalid",
+ "GIT_COMMITTER_NAME=newt test",
+ "GIT_COMMITTER_EMAIL=test@dummy.invalid")
+
+ runGit(t, env, nil, "-C", dir, "init", "-q")
+ runGit(t, env, nil, "-C", dir, "add", ".")
+ runGit(t, env, nil, "-C", dir, "commit", "-q", "-m", "project")
+ runGit(t, env, nil, "-C", dir, "remote", "add", "origin", origin)
+
+ return runGit(t, env, nil, "-C", dir, "rev-parse", "HEAD")
+}
+
+// A repo dependency with the same name as the project (e.g., the project is
+// a checkout of a repo that apache-mynewt-core depends on) must be satisfied
+// by the project itself and must not modify the project's git checkout.
+func TestLocalRepoDependency(t *testing.T) {
+ env := dummyReposEnv(t)
+ projDir := copyToTemp(t, localRepoDir)
+
+ const origin = "https://example.invalid/project.git"
+ head := localRepoGitInit(t, env, projDir, origin)
+
+ for i := 0; i < 2; i++ {
+ out, err := newtCombinedOutput(projDir, env, "upgrade")
+ if err != nil {
+ t.Fatalf("newt upgrade failed: %v\n%s", err, out)
+ }
+
+ gotOrigin := runGit(t, env, nil, "-C", projDir, "remote", "get-url",
+ "origin")
+ if gotOrigin != origin {
+ t.Fatalf("project origin changed to %q", gotOrigin)
+ }
+
+ gotHead := runGit(t, env, nil, "-C", projDir, "rev-parse", "HEAD")
+ if gotHead != head {
+ t.Fatalf("project HEAD changed to %s", gotHead)
+ }
+
+ checkOutput(t, "installed repos",
+ "mynewt-dummy-repo-01\nmynewt-dummy-repo-03\n"+
+ "mynewt-dummy-repo-04",
+ strings.Join(installedRepos(t, projDir), "\n"))
+ }
+}
diff --git a/newt/project/project.go b/newt/project/project.go
index 298abffd..4a967b49 100644
--- a/newt/project/project.go
+++ b/newt/project/project.go
@@ -601,6 +601,15 @@ func (proj *Project) loadRepoDeps(download bool) error {
var newRepos []*repo.Repo
for _, dep := range repoDepsForReqs(r, verReqs[r.Name()]) {
+ // A dependency with the same name as the project refers to
+ // the project itself (e.g., when the project is a checkout of
+ // that repo). The local repo is never downloaded or updated.
+ if dep.Name == proj.localRepo.Name() {
+ log.Debugf("Repo \"%s\" depends on \"%s\"; using the "+
+ "local project", r.Name(), dep.Name)
+ continue
+ }
+
if !addVerReq(dep.Name, dep.VerReqs) {
continue
}
diff --git a/newt/testdata/localrepo_dep/project.yml b/newt/testdata/localrepo_dep/project.yml
new file mode 100644
index 00000000..c118650f
--- /dev/null
+++ b/newt/testdata/localrepo_dep/project.yml
@@ -0,0 +1,27 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+#
+
+# The project has the same name as a repo that mynewt-dummy-repo-01 depends
+# on, i.e., it is a checkout of that repo.
+project.name: mynewt-dummy-repo-02
+
+repository.mynewt-dummy-repo-01:
+ type: git
+ vers: 0.0.0
+ url: 'https://dummy.invalid/mynewt-dummy-repo-01.git'
(mynewt-newt) 23/30: newt/extcmd: Fix panic on empty external command
Posted by ja...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
sjanc pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/mynewt-newt.git
commit 4d2e2e5bf05e92b562b459b3b938fae3b69ae8b9
Author: Szymon Janc <sz...@codecoup.pl>
AuthorDate: Mon Oct 19 17:46:32 2026 +0200
newt/extcmd: Fix panic on empty external command
An empty or whitespace-only key in pkg.pre_build_cmds, pre_link_cmds
or post_link_cmds produced no tokens after shell splitting and newt
panicked indexing the first token when running the command.
Reject such commands when reading the configuration, reporting the
package that defines them, and guard against an empty token list
before executing a command.
Assisted-by: Claude:claude-opus-5-5
---
newt/build_errors_test.go | 84 +++++++++++++++++++++++++++++++++++++++++++++++
newt/builder/extcmd.go | 4 +++
newt/extcmd/extcmd.go | 13 ++++++++
3 files changed, 101 insertions(+)
diff --git a/newt/build_errors_test.go b/newt/build_errors_test.go
new file mode 100644
index 00000000..eead14a1
--- /dev/null
+++ b/newt/build_errors_test.go
@@ -0,0 +1,84 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package main
+
+import (
+ "os"
+ "path/filepath"
+ "strings"
+ "testing"
+)
+
+// buildErrProject copies the syscfg_collision project to a temporary
+// directory and adds a compiler definition which does not need a real
+// compiler as long as nothing gets compiled.
+func buildErrProject(t *testing.T) string {
+ t.Helper()
+
+ dir := copyToTemp(t, filepath.Join("testdata", "syscfg_collision"))
+ yml := "compiler.path.cc: nonexistent-cc\n" +
+ "compiler.path.archive: nonexistent-ar\n" +
+ "compiler.flags.default: [-O1]\n"
+ path := filepath.Join(dir, "repos", "apache-mynewt-core", "compiler",
+ "sim", "compiler.yml")
+ if err := os.WriteFile(path, []byte(yml), 0644); err != nil {
+ t.Fatal(err)
+ }
+
+ return dir
+}
+
+// buildErrAppendFile appends text to a file.
+func buildErrAppendFile(t *testing.T, path string, text string) {
+ t.Helper()
+
+ f, err := os.OpenFile(path, os.O_APPEND|os.O_WRONLY, 0)
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer f.Close()
+
+ if _, err := f.WriteString(text); err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestBuildEmptyExtCmd(t *testing.T) {
+ for _, key := range []string{"pre_build_cmds", "pre_link_cmds",
+ "post_link_cmds"} {
+
+ t.Run(key, func(t *testing.T) {
+ dir := buildErrProject(t)
+ buildErrAppendFile(t, filepath.Join(dir, "apps", "app",
+ "pkg.yml"), "\npkg."+key+":\n ' ': 100\n")
+
+ out, err := newtCombinedOutput(dir, nil, "build", "success")
+ if err == nil {
+ t.Fatalf("newt build succeeded unexpectedly:\n%s", out)
+ }
+ if strings.Contains(out, "panic") {
+ t.Fatalf("newt build panicked:\n%s", out)
+ }
+ if !strings.Contains(out, "apps/app: invalid command") {
+ t.Fatalf("unexpected newt build output:\n%s", out)
+ }
+ })
+ }
+}
diff --git a/newt/builder/extcmd.go b/newt/builder/extcmd.go
index d14144cd..67bc62e5 100644
--- a/newt/builder/extcmd.go
+++ b/newt/builder/extcmd.go
@@ -157,6 +157,10 @@ func (t *TargetBuilder) execExtCmds(sf stage.StageFunc, userSrcDir string,
return util.FmtNewtError(
"invalid command string: \"%s\": %s", sf.Name, err.Error())
}
+ if len(toks) == 0 {
+ return util.FmtNewtError("empty command string in package %s",
+ sf.Pkg.FullName())
+ }
// Replace environment variables in command string.
for i, tok := range toks {
diff --git a/newt/extcmd/extcmd.go b/newt/extcmd/extcmd.go
index 7cd00427..6e51cbe1 100644
--- a/newt/extcmd/extcmd.go
+++ b/newt/extcmd/extcmd.go
@@ -21,6 +21,8 @@ package extcmd
import (
"fmt"
+
+ "github.com/kballard/go-shellquote"
"mynewt.apache.org/newt/newt/cfgv"
"mynewt.apache.org/newt/newt/pkg"
@@ -54,6 +56,17 @@ func (ecfg *ExtCmdCfg) readOnePkg(lpkg *pkg.LocalPackage, cfg *syscfg.Cfg,
cmdMap := getMapCb(lpkg, settings)
for name, stageStr := range cmdMap {
+ toks, err := shellquote.Split(name)
+ if err != nil || len(toks) == 0 {
+ text := fmt.Sprintf("%s: invalid command \"%s\"",
+ lpkg.FullName(), name)
+ if err != nil {
+ text += ": " + err.Error()
+ }
+ ecfg.InvalidSettings = append(ecfg.InvalidSettings, text)
+ continue
+ }
+
sf, err := stage.NewStageFunc(name, stageStr, lpkg, cfg)
if err != nil {
text := fmt.Sprintf("%s: %s", lpkg.FullName(), err.Error())
(mynewt-newt) 13/30: newt/resolve: Fix hang on non-converging syscfg
Posted by ja...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
sjanc pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/mynewt-newt.git
commit 6668f24c4316c681856f6d0bb3a28020fc3b94f4
Author: Szymon Janc <sz...@codecoup.pl>
AuthorDate: Mon Oct 19 17:53:26 2026 +0200
newt/resolve: Fix hang on non-converging syscfg
Dependency and syscfg resolution is repeated until the configuration
stops changing. Contradicting conditional overrides never converge,
e.g. a setting X overridden to 0 under the condition X, or a setting
whose override removes the package defining the setting that the
override depends on. newt then looped forever in "target config show",
"target dep", "build" etc.
Record the resolver state (settings and packages) after each iteration
and fail with an error listing the oscillating settings and packages
when a previously seen state repeats.
Assisted-by: Claude:claude-opus-5-5
---
newt/resolve/resolve.go | 105 +++++++++++++++++++++
newt/syscfg_stability_test.go | 64 +++++++++++++
.../testdata/syscfg_stability/apps/osc_dep/pkg.yml | 26 +++++
.../syscfg_stability/apps/osc_dep/syscfg.yml | 22 +++++
.../syscfg_stability/apps/osc_self/pkg.yml | 24 +++++
.../syscfg_stability/apps/osc_self/syscfg.yml | 22 +++++
.../repos/apache-mynewt-core/sys/osc/pkg.yml | 22 +++++
.../repos/apache-mynewt-core/sys/osc/syscfg.yml | 27 ++++++
.../repos/apache-mynewt-core/sys/oscbar/pkg.yml | 22 +++++
.../repos/apache-mynewt-core/sys/oscbar/syscfg.yml | 24 +++++
.../syscfg_stability/targets/osc_dep/pkg.yml | 22 +++++
.../syscfg_stability/targets/osc_dep/target.yml | 23 +++++
.../syscfg_stability/targets/osc_self/pkg.yml | 22 +++++
.../syscfg_stability/targets/osc_self/target.yml | 23 +++++
14 files changed, 448 insertions(+)
diff --git a/newt/resolve/resolve.go b/newt/resolve/resolve.go
index 4678ea6a..a12858b1 100644
--- a/newt/resolve/resolve.go
+++ b/newt/resolve/resolve.go
@@ -1023,6 +1023,98 @@ func (r *Resolver) resolveDeps() ([]*ResolvePackage, error) {
return rpkgs, nil
}
+// A snapshot of the resolver state used to detect configurations that never
+// converge.
+type resolveState struct {
+ // Setting name => value.
+ settings map[string]string
+
+ // Full names of the resolved packages.
+ pkgs map[string]struct{}
+
+ // Unique textual representation of the state.
+ key string
+}
+
+func (r *Resolver) curState() resolveState {
+ st := resolveState{
+ settings: make(map[string]string, len(r.cfg.Settings)),
+ pkgs: make(map[string]struct{}, len(r.pkgMap)),
+ }
+
+ lines := make([]string, 0, len(r.cfg.Settings)+len(r.pkgMap))
+ for k, entry := range r.cfg.Settings {
+ st.settings[k] = entry.Value
+ lines = append(lines, fmt.Sprintf("s:%s=%s:%d",
+ k, entry.Value, len(entry.History)))
+ }
+ for _, rpkg := range r.pkgMap {
+ name := rpkg.Lpkg.FullName()
+ st.pkgs[name] = struct{}{}
+ lines = append(lines, "p:"+name)
+ }
+ sort.Strings(lines)
+ st.key = strings.Join(lines, "\n")
+
+ return st
+}
+
+// Builds an error describing a resolution that oscillates between the
+// specified states without ever converging.
+func oscillationError(cycle []resolveState) error {
+ settingVals := map[string][]string{}
+ pkgSeen := map[string]int{}
+
+ for _, st := range cycle {
+ for k := range st.settings {
+ settingVals[k] = nil
+ }
+ for name := range st.pkgs {
+ pkgSeen[name]++
+ }
+ }
+
+ var settingLines []string
+ for k := range settingVals {
+ var vals []string
+ differ := false
+ for i, st := range cycle {
+ v, ok := st.settings[k]
+ if !ok {
+ v = "(undefined)"
+ }
+ if i > 0 && v != vals[0] {
+ differ = true
+ }
+ vals = append(vals, v)
+ }
+ if differ {
+ settingLines = append(settingLines,
+ fmt.Sprintf(" %s: %s", k, strings.Join(vals, " -> ")))
+ }
+ }
+ sort.Strings(settingLines)
+
+ var pkgLines []string
+ for name, count := range pkgSeen {
+ if count != len(cycle) {
+ pkgLines = append(pkgLines, " "+name)
+ }
+ }
+ sort.Strings(pkgLines)
+
+ str := "Syscfg does not converge; conditional settings or " +
+ "dependencies contradict each other"
+ if len(settingLines) > 0 {
+ str += "\nOscillating settings:\n" + strings.Join(settingLines, "\n")
+ }
+ if len(pkgLines) > 0 {
+ str += "\nOscillating packages:\n" + strings.Join(pkgLines, "\n")
+ }
+
+ return util.NewNewtError(str)
+}
+
// Performs a set of resolution actions:
// 1. Calculates the system configuration (syscfg).
// 2. Determines which packages satisfy which API requirements.
@@ -1032,6 +1124,12 @@ func (r *Resolver) resolveDepsAndCfg() error {
return err
}
+ // States seen so far; used to detect a configuration that never
+ // converges (e.g., a setting whose override disables its own
+ // condition).
+ var states []resolveState
+ stateIdx := map[string]int{}
+
for {
cfgChanged, err := r.reloadCfg()
if err != nil {
@@ -1054,6 +1152,13 @@ func (r *Resolver) resolveDepsAndCfg() error {
if !cfgChanged {
break
}
+
+ st := r.curState()
+ if i, ok := stateIdx[st.key]; ok {
+ return oscillationError(states[i:])
+ }
+ stateIdx[st.key] = len(states)
+ states = append(states, st)
}
// Now that the final set of packages is known, determine which ones
diff --git a/newt/syscfg_stability_test.go b/newt/syscfg_stability_test.go
index 59f5e57b..672d207a 100644
--- a/newt/syscfg_stability_test.go
+++ b/newt/syscfg_stability_test.go
@@ -20,9 +20,11 @@
package main
import (
+ "bytes"
"path/filepath"
"strings"
"testing"
+ "time"
)
var syscfgStabilityDir = filepath.Join("testdata", "syscfg_stability")
@@ -86,3 +88,65 @@ func TestSyscfgStabilityExtRepoTarget(t *testing.T) {
}
}
}
+
+// syscfgStabilityRunTimeout runs newt and kills it if it does not finish
+// within the timeout. Returns combined output and whether newt timed out.
+func syscfgStabilityRunTimeout(t *testing.T, dir string,
+ timeout time.Duration, args ...string) (string, bool) {
+
+ t.Helper()
+
+ cmd := newtExecCmd(dir, nil, args...)
+ var out bytes.Buffer
+ cmd.Stdout = &out
+ cmd.Stderr = &out
+
+ if err := cmd.Start(); err != nil {
+ t.Fatal(err)
+ }
+
+ done := make(chan struct{})
+ go func() {
+ cmd.Wait()
+ close(done)
+ }()
+
+ select {
+ case <-done:
+ return out.String(), false
+ case <-time.After(timeout):
+ cmd.Process.Kill()
+ <-done
+ return out.String(), true
+ }
+}
+
+// Conditional overrides that contradict each other are reported as an
+// error instead of making newt loop forever.
+func TestSyscfgStabilityOscillation(t *testing.T) {
+ tests := []struct {
+ target string
+ msgs []string
+ }{
+ {"osc_self", []string{"OSC_X: "}},
+ {"osc_dep", []string{"OSC_Y: ", "@apache-mynewt-core/sys/oscbar"}},
+ }
+
+ for _, tc := range tests {
+ t.Run(tc.target, func(t *testing.T) {
+ out, timedOut := syscfgStabilityRunTimeout(t, syscfgStabilityDir,
+ 30*time.Second, "target", "config", "show", tc.target)
+ if timedOut {
+ t.Fatalf("newt target config show did not finish\n%s", out)
+ }
+ syscfgStabilityCheckNoPanic(t, out)
+
+ msgs := append([]string{"Syscfg does not converge"}, tc.msgs...)
+ for _, msg := range msgs {
+ if !strings.Contains(out, msg) {
+ t.Errorf("missing \"%s\" in output\n%s", msg, out)
+ }
+ }
+ })
+ }
+}
diff --git a/newt/testdata/syscfg_stability/apps/osc_dep/pkg.yml b/newt/testdata/syscfg_stability/apps/osc_dep/pkg.yml
new file mode 100644
index 00000000..6762e7d3
--- /dev/null
+++ b/newt/testdata/syscfg_stability/apps/osc_dep/pkg.yml
@@ -0,0 +1,26 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+#
+
+
+pkg.name: apps/osc_dep
+pkg.type: app
+pkg.deps:
+ - "@apache-mynewt-core/sys/osc"
+pkg.deps.'!OSC_Y':
+ - "@apache-mynewt-core/sys/oscbar"
diff --git a/newt/testdata/syscfg_stability/apps/osc_dep/syscfg.yml b/newt/testdata/syscfg_stability/apps/osc_dep/syscfg.yml
new file mode 100644
index 00000000..999e8913
--- /dev/null
+++ b/newt/testdata/syscfg_stability/apps/osc_dep/syscfg.yml
@@ -0,0 +1,22 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+#
+
+
+syscfg.vals.OSC_BARDEF:
+ OSC_Y: 1
diff --git a/newt/testdata/syscfg_stability/apps/osc_self/pkg.yml b/newt/testdata/syscfg_stability/apps/osc_self/pkg.yml
new file mode 100644
index 00000000..62008371
--- /dev/null
+++ b/newt/testdata/syscfg_stability/apps/osc_self/pkg.yml
@@ -0,0 +1,24 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+#
+
+
+pkg.name: apps/osc_self
+pkg.type: app
+pkg.deps:
+ - "@apache-mynewt-core/sys/osc"
diff --git a/newt/testdata/syscfg_stability/apps/osc_self/syscfg.yml b/newt/testdata/syscfg_stability/apps/osc_self/syscfg.yml
new file mode 100644
index 00000000..990b6307
--- /dev/null
+++ b/newt/testdata/syscfg_stability/apps/osc_self/syscfg.yml
@@ -0,0 +1,22 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+#
+
+
+syscfg.vals.OSC_X:
+ OSC_X: 0
diff --git a/newt/testdata/syscfg_stability/repos/apache-mynewt-core/sys/osc/pkg.yml b/newt/testdata/syscfg_stability/repos/apache-mynewt-core/sys/osc/pkg.yml
new file mode 100644
index 00000000..d63851e4
--- /dev/null
+++ b/newt/testdata/syscfg_stability/repos/apache-mynewt-core/sys/osc/pkg.yml
@@ -0,0 +1,22 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+#
+
+
+pkg.name: sys/osc
+pkg.type: lib
diff --git a/newt/testdata/syscfg_stability/repos/apache-mynewt-core/sys/osc/syscfg.yml b/newt/testdata/syscfg_stability/repos/apache-mynewt-core/sys/osc/syscfg.yml
new file mode 100644
index 00000000..a506be43
--- /dev/null
+++ b/newt/testdata/syscfg_stability/repos/apache-mynewt-core/sys/osc/syscfg.yml
@@ -0,0 +1,27 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+#
+
+
+syscfg.defs:
+ OSC_X:
+ description: Setting disabling its own override condition.
+ value: 1
+ OSC_Y:
+ description: Setting controlling a dependency.
+ value: 0
diff --git a/newt/testdata/syscfg_stability/repos/apache-mynewt-core/sys/oscbar/pkg.yml b/newt/testdata/syscfg_stability/repos/apache-mynewt-core/sys/oscbar/pkg.yml
new file mode 100644
index 00000000..a97c2ae7
--- /dev/null
+++ b/newt/testdata/syscfg_stability/repos/apache-mynewt-core/sys/oscbar/pkg.yml
@@ -0,0 +1,22 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+#
+
+
+pkg.name: sys/oscbar
+pkg.type: lib
diff --git a/newt/testdata/syscfg_stability/repos/apache-mynewt-core/sys/oscbar/syscfg.yml b/newt/testdata/syscfg_stability/repos/apache-mynewt-core/sys/oscbar/syscfg.yml
new file mode 100644
index 00000000..21dbf0c5
--- /dev/null
+++ b/newt/testdata/syscfg_stability/repos/apache-mynewt-core/sys/oscbar/syscfg.yml
@@ -0,0 +1,24 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+#
+
+
+syscfg.defs:
+ OSC_BARDEF:
+ description: Setting defined only if sys/oscbar is included.
+ value: 1
diff --git a/newt/testdata/syscfg_stability/targets/osc_dep/pkg.yml b/newt/testdata/syscfg_stability/targets/osc_dep/pkg.yml
new file mode 100644
index 00000000..7860a09a
--- /dev/null
+++ b/newt/testdata/syscfg_stability/targets/osc_dep/pkg.yml
@@ -0,0 +1,22 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+#
+
+
+pkg.name: targets/osc_dep
+pkg.type: target
diff --git a/newt/testdata/syscfg_stability/targets/osc_dep/target.yml b/newt/testdata/syscfg_stability/targets/osc_dep/target.yml
new file mode 100644
index 00000000..9bf20f18
--- /dev/null
+++ b/newt/testdata/syscfg_stability/targets/osc_dep/target.yml
@@ -0,0 +1,23 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+#
+
+
+target.app: apps/osc_dep
+target.bsp: "@apache-mynewt-core/hw/bsp/sim"
+target.build_profile: default
diff --git a/newt/testdata/syscfg_stability/targets/osc_self/pkg.yml b/newt/testdata/syscfg_stability/targets/osc_self/pkg.yml
new file mode 100644
index 00000000..e4ecb281
--- /dev/null
+++ b/newt/testdata/syscfg_stability/targets/osc_self/pkg.yml
@@ -0,0 +1,22 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+#
+
+
+pkg.name: targets/osc_self
+pkg.type: target
diff --git a/newt/testdata/syscfg_stability/targets/osc_self/target.yml b/newt/testdata/syscfg_stability/targets/osc_self/target.yml
new file mode 100644
index 00000000..59eedc73
--- /dev/null
+++ b/newt/testdata/syscfg_stability/targets/osc_self/target.yml
@@ -0,0 +1,23 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+#
+
+
+target.app: apps/osc_self
+target.bsp: "@apache-mynewt-core/hw/bsp/sim"
+target.build_profile: default
(mynewt-newt) 29/30: newt/toolchain: Fix builds of projects in paths with spaces
Posted by ja...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
sjanc pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/mynewt-newt.git
commit 60166292071681168ef30fdf5cfc3e8558a5a3d9
Author: Szymon Janc <sz...@codecoup.pl>
AuthorDate: Mon Oct 19 18:00:11 2026 +0200
newt/toolchain: Fix builds of projects in paths with spaces
Include paths were passed through util.SortFields(), which splits
strings on white space, so every include path below a directory with
a space in its name was broken into several bogus arguments and no
project in such a directory could be built.
Sort and deduplicate include paths without splitting them. Commands
are executed with argument slices already; quote arguments containing
white space, quotes or backslashes where newt generates text that is
parsed later: the object file list passed to the linker as @file, the
commands in compile_commands.json and the include directories in the
CMake output.
Assisted-by: Claude:claude-opus-5-5
---
newt/build_space_test.go | 104 +++++++++++++++++++++++++++++++++++++++++++
newt/builder/cmake.go | 12 ++++-
newt/toolchain/compiler.go | 38 ++++++++++++++--
newt/toolchain/flags_test.go | 74 ++++++++++++++++++++++++++++++
4 files changed, 223 insertions(+), 5 deletions(-)
diff --git a/newt/build_space_test.go b/newt/build_space_test.go
new file mode 100644
index 00000000..55a31497
--- /dev/null
+++ b/newt/build_space_test.go
@@ -0,0 +1,104 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package main
+
+import (
+ "os"
+ "os/exec"
+ "path/filepath"
+ "runtime"
+ "testing"
+
+ "github.com/otiai10/copy"
+)
+
+// buildSpaceWriteFile writes a file, creating its directory if needed.
+func buildSpaceWriteFile(t *testing.T, path string, content string) {
+ t.Helper()
+
+ if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
+ t.Fatal(err)
+ }
+ if err := os.WriteFile(path, []byte(content), 0644); err != nil {
+ t.Fatal(err)
+ }
+}
+
+// TestBuildSpaceInPath builds a project located in a directory with a space
+// in its name, including a source file with a space in its name, using the
+// host C compiler.
+func TestBuildSpaceInPath(t *testing.T) {
+ if runtime.GOOS == "windows" {
+ t.Skip("host build not supported on Windows")
+ }
+
+ var cc string
+ for _, name := range []string{"cc", "gcc"} {
+ if path, err := exec.LookPath(name); err == nil {
+ cc = path
+ break
+ }
+ }
+ if cc == "" {
+ t.Skip("no C compiler found in PATH")
+ }
+
+ dir := filepath.Join(t.TempDir(), "sp ace", "proj")
+ if err := copy.Copy(filepath.Join("testdata", "syscfg_collision"),
+ dir); err != nil {
+
+ t.Fatal(err)
+ }
+
+ core := filepath.Join(dir, "repos", "apache-mynewt-core")
+ buildSpaceWriteFile(t, filepath.Join(core, "compiler", "sim",
+ "compiler.yml"),
+ "compiler.path.cc: \""+cc+"\"\n"+
+ "compiler.path.as: \""+cc+"\"\n"+
+ "compiler.path.archive: ar\n"+
+ "compiler.path.objsize: size\n"+
+ "compiler.flags.default: [-O1, -Wall]\n"+
+ "compiler.ld.binfile: false\n")
+ buildSpaceWriteFile(t, filepath.Join(core, "sys", "foo", "src",
+ "foo.c"), "void foo_init(void) {}\n")
+ // Linked as a list of object files (@file).
+ buildErrAppendFile(t, filepath.Join(core, "sys", "foo", "pkg.yml"),
+ "\npkg.whole_archive: true\n")
+ buildSpaceWriteFile(t, filepath.Join(core, "sys", "foo", "include",
+ "foo dir", "foo.h"), "#define FOO 1\n")
+ buildSpaceWriteFile(t, filepath.Join(dir, "targets", "success",
+ "include", "flash_map", "flash_map.h"),
+ "#include <stdint.h>\n"+
+ "struct flash_area { uint8_t fa_id; uint8_t fa_device_id;\n"+
+ " uint16_t pad16; uint32_t fa_off; uint32_t fa_size; };\n")
+ buildSpaceWriteFile(t, filepath.Join(dir, "apps", "app", "src",
+ "main.c"), "#include \"foo dir/foo.h\"\n"+
+ "int main(void) { return FOO - 1; }\n")
+ buildSpaceWriteFile(t, filepath.Join(dir, "apps", "app", "src",
+ "my file.c"), "int my_file(void) { return 0; }\n")
+
+ // Second and third builds use the generated dependency files.
+ for i := 0; i < 3; i++ {
+ out, err := newtCombinedOutput(dir, nil, "build", "success")
+ if err != nil {
+ t.Fatalf("newt build #%d failed: %s\n%s", i+1, err, out)
+ }
+ }
+}
diff --git a/newt/builder/cmake.go b/newt/builder/cmake.go
index 6e5a37b3..876d090c 100644
--- a/newt/builder/cmake.go
+++ b/newt/builder/cmake.go
@@ -27,6 +27,7 @@ import (
"os"
"path"
"path/filepath"
+ "sort"
"strings"
log "github.com/sirupsen/logrus"
@@ -349,10 +350,17 @@ func CmakeCompilerInfoWrite(w io.Writer, archiveFile string, bpkg *BuildPackage,
includes = append(includes, c.GetLocalCompilerInfo().Includes...)
includes = append(includes, otherIncludes...)
- // Sort and remove duplicate flags
- includes = util.SortFields(includes...)
+ // Sort and remove duplicate paths; paths may contain spaces, so they
+ // must not be split.
+ includes = util.UniqueStrings(includes)
+ sort.Strings(includes)
trimProjectPathSlice(includes)
replaceBackslashesSlice(includes)
+ for i, inc := range includes {
+ if strings.ContainsAny(inc, " \t") {
+ includes[i] = "\"" + inc + "\""
+ }
+ }
fmt.Fprintf(w,
`set_target_properties(%s
diff --git a/newt/toolchain/compiler.go b/newt/toolchain/compiler.go
index 43b8e613..f1537ac2 100644
--- a/newt/toolchain/compiler.go
+++ b/newt/toolchain/compiler.go
@@ -133,11 +133,34 @@ func (c *Compiler) addCompileCommand(cmd []string, file string) {
c.compileCommands = append(c.compileCommands,
CompileCommand{
- Command: strings.Join(cmd, " "),
+ Command: commandString(cmd),
File: file,
})
}
+// quoteArg quotes a command argument containing white space, quotes or
+// backslashes, so that it is not split when the command line or a response
+// file (@file) is parsed. Other arguments are returned unchanged.
+func quoteArg(arg string) string {
+ if !strings.ContainsAny(arg, " \t\n\"'\\") {
+ return arg
+ }
+
+ r := strings.NewReplacer("\\", "\\\\", "\"", "\\\"")
+ return "\"" + r.Replace(arg) + "\""
+}
+
+// commandString joins command arguments into a single command line, quoting
+// arguments where necessary.
+func commandString(cmd []string) string {
+ args := make([]string, len(cmd))
+ for i, arg := range cmd {
+ args[i] = quoteArg(arg)
+ }
+
+ return strings.Join(args, " ")
+}
+
func (c *Compiler) GetCcPath() string {
return c.ccPath
}
@@ -531,7 +554,16 @@ func (c *Compiler) includesStrings() []string {
return nil
}
- includes := util.SortFields(c.info.Includes...)
+ // Include paths may contain spaces; do not split them.
+ includes := []string{}
+ for _, s := range c.info.Includes {
+ s = strings.TrimSpace(s)
+ if s != "" {
+ includes = append(includes, s)
+ }
+ }
+ includes = util.UniqueStrings(includes)
+ sort.Strings(includes)
tokens := make([]string, len(includes))
for i, s := range includes {
@@ -1578,7 +1610,7 @@ func (c *Compiler) CompileArchive(outputFile string, createList bool) error {
defer file.Close()
for _, objFilePath := range objList {
- _, err = file.WriteString(objFilePath + "\n")
+ _, err = file.WriteString(quoteArg(objFilePath) + "\n")
if err != nil {
return util.NewNewtError(err.Error())
}
diff --git a/newt/toolchain/flags_test.go b/newt/toolchain/flags_test.go
new file mode 100644
index 00000000..a93e511f
--- /dev/null
+++ b/newt/toolchain/flags_test.go
@@ -0,0 +1,74 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package toolchain
+
+import (
+ "slices"
+ "testing"
+
+ "github.com/kballard/go-shellquote"
+)
+
+func TestIncludesStringsSpaces(t *testing.T) {
+ c := newTestCompiler()
+ c.baseDir = "/sp ace/proj"
+ c.info.Includes = []string{
+ "/sp ace/proj/b dir/include",
+ "/sp ace/proj/a/include",
+ "/other dir/include",
+ "/sp ace/proj/a/include",
+ " ",
+ }
+
+ exp := []string{
+ "-I/other dir/include",
+ "-Ia/include",
+ "-Ib dir/include",
+ }
+ if got := c.includesStrings(); !slices.Equal(got, exp) {
+ t.Errorf("unexpected include flags: expected %q, got %q", exp, got)
+ }
+}
+
+func TestCommandString(t *testing.T) {
+ cmd := []string{
+ "gcc",
+ "-DSTR=\"a b\"",
+ "-I/sp ace/inc",
+ "-DQ='x'",
+ "-c",
+ "a\\b.c",
+ "plain.c",
+ }
+
+ s := commandString(cmd)
+ got, err := shellquote.Split(s)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if !slices.Equal(got, cmd) {
+ t.Errorf("round trip failed: %q -> %q -> %q", cmd, s, got)
+ }
+
+ // Arguments without special characters are not quoted.
+ if s := commandString([]string{"gcc", "-O2", "a.c"}); s != "gcc -O2 a.c" {
+ t.Errorf("unexpected command string: %q", s)
+ }
+}
(mynewt-newt) 02/30: newt/install: Fix panic on long answer to upgrade prompt
Posted by ja...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
sjanc pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/mynewt-newt.git
commit 07d10d463aa3413f9545f858a10af44b9ae05377
Author: Szymon Janc <sz...@codecoup.pl>
AuthorDate: Mon Oct 19 17:46:37 2026 +0200
newt/install: Fix panic on long answer to upgrade prompt
The "Proceed? [Y/n]" prompt read the answer with bufio ReadLine. For
a line longer than the reader buffer ReadLine returns more=true with
a nil error, which was then passed to util.ChildNewtError(nil) and
caused a nil pointer dereference.
Read whole lines with ReadString instead, using a single reader for
all attempts, so a long answer is treated as an invalid response.
An answer terminated by EOF without newline is still accepted.
Assisted-by: Claude:claude-opus-5-5
---
newt/install/install.go | 13 +++++++--
newt/install/install_test.go | 68 ++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 78 insertions(+), 3 deletions(-)
diff --git a/newt/install/install.go b/newt/install/install.go
index 36faf4fd..2d48eea5 100644
--- a/newt/install/install.go
+++ b/newt/install/install.go
@@ -66,6 +66,7 @@ package install
import (
"bufio"
"fmt"
+ "io"
"os"
"strings"
@@ -376,14 +377,20 @@ func (inst *Installer) installPrompt(vm deprepo.VersionMap, op installOp,
}
}
+ return askProceed(bufio.NewReader(os.Stdin))
+}
+
+// Asks the user whether to proceed and reads the answer from the specified
+// reader. Returns true if the user wants to proceed.
+func askProceed(rd *bufio.Reader) (bool, error) {
for {
fmt.Printf("Proceed? [Y/n] ")
- line, more, err := bufio.NewReader(os.Stdin).ReadLine()
- if more || err != nil {
+ line, err := rd.ReadString('\n')
+ if err != nil && (err != io.EOF || len(line) == 0) {
return false, util.ChildNewtError(err)
}
- trimmed := strings.ToLower(strings.TrimSpace(string(line)))
+ trimmed := strings.ToLower(strings.TrimSpace(line))
if len(trimmed) == 0 || strings.HasPrefix(trimmed, "y") {
// User wants to proceed.
return true, nil
diff --git a/newt/install/install_test.go b/newt/install/install_test.go
new file mode 100644
index 00000000..2d776cd4
--- /dev/null
+++ b/newt/install/install_test.go
@@ -0,0 +1,68 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package install
+
+import (
+ "bufio"
+ "strings"
+ "testing"
+)
+
+func TestAskProceed(t *testing.T) {
+ long := strings.Repeat("x", 10000)
+
+ tests := []struct {
+ name string
+ input string
+ proceed bool
+ err bool
+ }{
+ {"empty", "\n", true, false},
+ {"yes", "y\n", true, false},
+ {"no", "n\n", false, false},
+ {"yes without newline", "Y", true, false},
+ {"eof", "", false, true},
+ {"invalid then no", "foo\nn\n", false, false},
+ {"long line then yes", long + "\ny\n", true, false},
+ {"long line then no", long + "\nno\n", false, false},
+ {"long yes line", "y" + long + "\n", true, false},
+ {"long line then eof", long, false, true},
+ }
+
+ for _, tc := range tests {
+ t.Run(tc.name, func(t *testing.T) {
+ rd := bufio.NewReader(strings.NewReader(tc.input))
+ proceed, err := askProceed(rd)
+ if tc.err {
+ if err == nil {
+ t.Fatalf("expected error")
+ }
+ return
+ }
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ if proceed != tc.proceed {
+ t.Fatalf("expected proceed=%v, got %v",
+ tc.proceed, proceed)
+ }
+ })
+ }
+}
(mynewt-newt) 18/30: newt: Fix panic in pkg new and docs build outside of project
Posted by ja...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
sjanc pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/mynewt-newt.git
commit ad2da0c1cfa52b9eac5099ea95f45afda6af5c8b
Author: Szymon Janc <sz...@codecoup.pl>
AuthorDate: Mon Oct 19 17:50:43 2026 +0200
newt: Fix panic in pkg new and docs build outside of project
"newt pkg new" and "newt docs build" used project.GetProject(), which
panics if no project is found, e.g. when run outside of a project
directory. Use TryGetProject() and report the error instead, as other
commands do.
Assisted-by: Claude:claude-opus-5-5
---
newt/cli/pkg_cmds.go | 5 ++++-
newt/cli_errors_test.go | 8 ++++++++
newt/docs/docsbuilder.go | 7 ++++++-
newt/project/pkgwriter.go | 9 ++++++---
4 files changed, 24 insertions(+), 5 deletions(-)
diff --git a/newt/cli/pkg_cmds.go b/newt/cli/pkg_cmds.go
index da7b3740..97d089eb 100644
--- a/newt/cli/pkg_cmds.go
+++ b/newt/cli/pkg_cmds.go
@@ -48,7 +48,10 @@ func pkgNewCmd(cmd *cobra.Command, args []string) {
NewTypeStr = strings.ToUpper(NewTypeStr)
- pw := project.NewPackageWriter()
+ pw, err := project.NewPackageWriter()
+ if err != nil {
+ NewtUsage(nil, err)
+ }
if err := pw.ConfigurePackage(NewTypeStr, args[0]); err != nil {
NewtUsage(cmd, err)
}
diff --git a/newt/cli_errors_test.go b/newt/cli_errors_test.go
index 2c1553df..5172af73 100644
--- a/newt/cli_errors_test.go
+++ b/newt/cli_errors_test.go
@@ -62,3 +62,11 @@ func TestCliPkgUnknownSrcRepo(t *testing.T) {
"pkg", op, "@nosuch/foo", "apps/bar")
}
}
+
+// Commands requiring a project must fail gracefully outside of one.
+func TestCliNoProject(t *testing.T) {
+ dir := t.TempDir()
+
+ cliErrorCheck(t, dir, nil, "No project file found", "pkg", "new", "foo")
+ cliErrorCheck(t, dir, nil, "No project file found", "docs", "build")
+}
diff --git a/newt/docs/docsbuilder.go b/newt/docs/docsbuilder.go
index 12e923d0..8ca49503 100644
--- a/newt/docs/docsbuilder.go
+++ b/newt/docs/docsbuilder.go
@@ -39,8 +39,13 @@ type DocDescriptor struct {
}
func NewDocsBuilder() (*DocsBuilder, error) {
+ proj, err := project.TryGetProject()
+ if err != nil {
+ return nil, err
+ }
+
db := &DocsBuilder{}
- db.project = project.GetProject()
+ db.project = proj
return db, nil
}
diff --git a/newt/project/pkgwriter.go b/newt/project/pkgwriter.go
index 0501599f..7967324f 100644
--- a/newt/project/pkgwriter.go
+++ b/newt/project/pkgwriter.go
@@ -274,13 +274,16 @@ func (pw *PackageWriter) WritePackage() error {
/**
* Create new PackageWriter structure, and return it
*/
-func NewPackageWriter() *PackageWriter {
- proj := GetProject()
+func NewPackageWriter() (*PackageWriter, error) {
+ proj, err := TryGetProject()
+ if err != nil {
+ return nil, err
+ }
pw := &PackageWriter{
project: proj,
downloader: downloader.NewGithubDownloader(),
}
- return pw
+ return pw, nil
}
(mynewt-newt) 30/30: newt/toolchain: Keep options with separate arguments together
Posted by ja...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
sjanc pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/mynewt-newt.git
commit 2ed032d118548b2d43f091c3ba7c846dce4cf0b9
Author: Szymon Janc <sz...@codecoup.pl>
AuthorDate: Mon Oct 19 18:01:31 2026 +0200
newt/toolchain: Keep options with separate arguments together
Compiler and linker flags were passed through util.SortFields(), which
sorts all white space separated tokens and removes duplicates. Options
taking a separate argument were torn apart from it, e.g.
"-include a.h -include b.h" became "-include a.h b.h", making the
compiler treat b.h as an input file. In addition, when such an option
was given as a separate list element, addFlags() discarded it as a
conflict if another package already used the same option, leaving its
argument behind.
Add SortFlags(), which keeps options like -include, -imacros,
-isystem or -Xlinker together with their argument, in their original
order and without deduplication, and sorts and deduplicates all other
flags as before. Use it for the compiler command lines and the CMake
output, and let addFlags() add such option pairs as a unit.
Assisted-by: Claude:claude-opus-5-5
---
newt/builder/cmake.go | 10 ++---
newt/toolchain/compiler.go | 23 ++++++++---
newt/toolchain/flags.go | 93 ++++++++++++++++++++++++++++++++++++++++++++
newt/toolchain/flags_test.go | 37 ++++++++++++++++++
4 files changed, 152 insertions(+), 11 deletions(-)
diff --git a/newt/builder/cmake.go b/newt/builder/cmake.go
index 876d090c..ca3038c9 100644
--- a/newt/builder/cmake.go
+++ b/newt/builder/cmake.go
@@ -141,7 +141,7 @@ func CmakeSourceObjectWrite(w io.Writer, cj toolchain.CompilerJob,
cj.Filename = trimProjectPath(cj.Filename)
// Sort and remove duplicate flags
- otherFlags = util.SortFields(otherFlags...)
+ otherFlags = toolchain.SortFlags(otherFlags...)
fmt.Fprintf(w,
`set_property(SOURCE %s APPEND_STRING
@@ -271,7 +271,7 @@ func (b *Builder) CMakeTargetWrite(w io.Writer, targetCompiler *toolchain.Compil
compileFlags = append(compileFlags, c.GetLocalCompilerInfo().Cflags...)
compileFlags = append(compileFlags, c.GetCompilerInfo().CXXflags...)
compileFlags = append(compileFlags, c.GetLocalCompilerInfo().CXXflags...)
- compileFlags = util.SortFields(compileFlags...)
+ compileFlags = toolchain.SortFlags(compileFlags...)
fmt.Fprintf(w,
`set_property(TARGET %s APPEND_STRING
@@ -284,7 +284,7 @@ func (b *Builder) CMakeTargetWrite(w io.Writer, targetCompiler *toolchain.Compil
lFlags := append(c.GetCompilerInfo().Lflags, c.GetLocalCompilerInfo().Lflags...)
lFlags = append(lFlags, linkFlags...)
- lFlags = util.SortFields(lFlags...)
+ lFlags = toolchain.SortFlags(lFlags...)
for _, ld := range c.LinkerScripts {
lFlags = append(lFlags, "-T"+ld)
@@ -295,13 +295,13 @@ func (b *Builder) CMakeTargetWrite(w io.Writer, targetCompiler *toolchain.Compil
var cFlags []string
cFlags = append(cFlags, c.GetCompilerInfo().Cflags...)
cFlags = append(cFlags, c.GetLocalCompilerInfo().Cflags...)
- cFlags = util.SortFields(cFlags...)
+ cFlags = toolchain.SortFlags(cFlags...)
lFlags = append(lFlags, cFlags...)
var cxxFlags []string
cxxFlags = append(cxxFlags, c.GetCompilerInfo().CXXflags...)
cxxFlags = append(cxxFlags, c.GetLocalCompilerInfo().CXXflags...)
- cxxFlags = util.SortFields(cxxFlags...)
+ cxxFlags = toolchain.SortFlags(cxxFlags...)
lFlags = append(lFlags, cxxFlags...)
fmt.Fprintf(w,
diff --git a/newt/toolchain/compiler.go b/newt/toolchain/compiler.go
index f1537ac2..306818b1 100644
--- a/newt/toolchain/compiler.go
+++ b/newt/toolchain/compiler.go
@@ -298,7 +298,18 @@ func addFlags(flagType string, orig []string, new []string) []string {
origMap := flagsMap(orig)
combined := orig
- for _, c := range new {
+ for i := 0; i < len(new); i++ {
+ c := new[i]
+
+ // An option taking a separate argument (e.g., "-include", "a.h") is
+ // added together with its argument; the option alone does not
+ // identify a conflict.
+ if flagTakesArg(new, i) {
+ combined = append(combined, c, new[i+1])
+ i++
+ continue
+ }
+
newBase := flagsBase(c)
origVal := origMap[newBase]
if origVal == "" {
@@ -575,9 +586,9 @@ func (c *Compiler) includesStrings() []string {
}
func (c *Compiler) cflagsStrings() []string {
- cflags := util.SortFields(c.info.Cflags...)
+ cflags := SortFlags(c.info.Cflags...)
- for _, lclinfo_flag := range util.SortFields(c.lclInfo.Cflags...) {
+ for _, lclinfo_flag := range SortFlags(c.lclInfo.Cflags...) {
for _, info_flag := range cflags {
if lclinfo_flag == info_flag {
continue
@@ -589,17 +600,17 @@ func (c *Compiler) cflagsStrings() []string {
}
func (c *Compiler) cxxflagsStrings() []string {
- cxxflags := util.SortFields(c.info.CXXflags...)
+ cxxflags := SortFlags(c.info.CXXflags...)
return cxxflags
}
func (c *Compiler) aflagsStrings() []string {
- aflags := util.SortFields(c.info.Aflags...)
+ aflags := SortFlags(c.info.Aflags...)
return aflags
}
func (c *Compiler) lflagsStrings() []string {
- lflags := util.SortFields(c.info.Lflags...)
+ lflags := SortFlags(c.info.Lflags...)
return lflags
}
diff --git a/newt/toolchain/flags.go b/newt/toolchain/flags.go
new file mode 100644
index 00000000..fa3460f9
--- /dev/null
+++ b/newt/toolchain/flags.go
@@ -0,0 +1,93 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package toolchain
+
+import (
+ "sort"
+ "strings"
+
+ "mynewt.apache.org/newt/util"
+)
+
+// Options which take their argument as a separate command line argument
+// (e.g., "-include file.h").
+var flagsWithArg = map[string]bool{
+ "--param": true,
+ "-D": true,
+ "-I": true,
+ "-L": true,
+ "-MF": true,
+ "-MQ": true,
+ "-MT": true,
+ "-T": true,
+ "-U": true,
+ "-Xassembler": true,
+ "-Xlinker": true,
+ "-Xpreprocessor": true,
+ "-idirafter": true,
+ "-imacros": true,
+ "-imultilib": true,
+ "-include": true,
+ "-iprefix": true,
+ "-iquote": true,
+ "-isysroot": true,
+ "-isystem": true,
+ "-iwithprefix": true,
+ "-iwithprefixbefore": true,
+ "-o": true,
+ "-u": true,
+ "-x": true,
+ "-z": true,
+}
+
+// flagTakesArg indicates whether flags[i] is an option followed by its
+// separate argument.
+func flagTakesArg(flags []string, i int) bool {
+ return flagsWithArg[flags[i]] && i+1 < len(flags)
+}
+
+// SortFlags splits the specified strings on white space, sorts the resulting
+// flags and removes duplicates. Options taking a separate argument are kept
+// together with their argument; these pairs are placed after all other flags
+// in their original order and are not deduplicated, because their order and
+// count may be significant (e.g., "-include a.h -include b.h" or
+// "-Xlinker -z -Xlinker relro").
+func SortFlags(flags ...string) []string {
+ tokens := []string{}
+ for _, f := range flags {
+ tokens = append(tokens, strings.Fields(f)...)
+ }
+
+ singles := []string{}
+ pairs := []string{}
+ for i := 0; i < len(tokens); i++ {
+ if flagTakesArg(tokens, i) {
+ pairs = append(pairs, tokens[i], tokens[i+1])
+ i++
+ } else {
+ singles = append(singles, tokens[i])
+ }
+ }
+
+ singles = util.UniqueStrings(singles)
+ sort.Strings(singles)
+
+ return append(singles, pairs...)
+}
diff --git a/newt/toolchain/flags_test.go b/newt/toolchain/flags_test.go
index a93e511f..fb25da16 100644
--- a/newt/toolchain/flags_test.go
+++ b/newt/toolchain/flags_test.go
@@ -72,3 +72,40 @@ func TestCommandString(t *testing.T) {
t.Errorf("unexpected command string: %q", s)
}
}
+
+func TestSortFlags(t *testing.T) {
+ got := SortFlags("-Wall -O2", "-include a.h", "-Wall",
+ "-include", "b.h", "-Xlinker -z -Xlinker relro",
+ "-Xlinker -z -Xlinker noexecstack", "-DFOO=1")
+
+ exp := []string{
+ "-DFOO=1", "-O2", "-Wall",
+ "-include", "a.h", "-include", "b.h",
+ "-Xlinker", "-z", "-Xlinker", "relro",
+ "-Xlinker", "-z", "-Xlinker", "noexecstack",
+ }
+ if !slices.Equal(got, exp) {
+ t.Errorf("unexpected flags:\nexpected %q\ngot %q", exp, got)
+ }
+
+ // An option without argument at the end is kept.
+ got = SortFlags("-b", "-a", "-include")
+ exp = []string{"-a", "-b", "-include"}
+ if !slices.Equal(got, exp) {
+ t.Errorf("unexpected flags:\nexpected %q\ngot %q", exp, got)
+ }
+}
+
+func TestCflagsStringsPairs(t *testing.T) {
+ c := newTestCompiler()
+
+ // Flags from two packages, both using forced includes as separate list
+ // elements.
+ c.AddInfo(&CompilerInfo{Cflags: []string{"-Wall", "-include", "a.h"}})
+ c.AddInfo(&CompilerInfo{Cflags: []string{"-include", "b.h", "-Wall"}})
+
+ exp := []string{"-Wall", "-include", "a.h", "-include", "b.h"}
+ if got := c.cflagsStrings(); !slices.Equal(got, exp) {
+ t.Errorf("unexpected flags:\nexpected %q\ngot %q", exp, got)
+ }
+}
(mynewt-newt) 04/30: newt/config: Fix panic on relative import with no project loaded
Posted by ja...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
sjanc pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/mynewt-newt.git
commit 12388a643a53785aaec047625d483eb0e82777a0
Author: Szymon Janc <sz...@codecoup.pl>
AuthorDate: Mon Oct 19 17:47:43 2026 +0200
newt/config: Fix panic on relative import with no project loaded
Relative $import paths were always resolved against the project
base. newtrc.yml is read before a project is loaded, so a relative
import in ~/.newt/newtrc.yml dereferenced a nil project and newt
crashed at startup of every command.
When no project is loaded, resolve relative imports against the
directory of the importing file, and report an error for imports
referring to a repository ("@repo/...").
Assisted-by: Claude:claude-opus-5-5
---
newt/config/config.go | 20 ++++++++---
newt/config/config_test.go | 87 ++++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 102 insertions(+), 5 deletions(-)
diff --git a/newt/config/config.go b/newt/config/config.go
index 1fe59ba2..99bb3d24 100644
--- a/newt/config/config.go
+++ b/newt/config/config.go
@@ -147,14 +147,24 @@ func readLineage(path string) ([]FileEntry, error) {
// Recursively process imports, accumulating file info in `entries`.
var iter func(path string, parent *util.FileInfo) error
iter = func(path string, parent *util.FileInfo) error {
- // Relative paths are relative to the project base.
+ // Relative paths are relative to the project base. If no project
+ // is loaded (e.g., when reading newtrc), imports are relative to
+ // the importing file.
if !filepath.IsAbs(path) {
proj := interfaces.GetProject()
- newPath, err := proj.ResolvePath(proj.Path(), path)
- if err != nil {
- return err
+ if proj != nil {
+ newPath, err := proj.ResolvePath(proj.Path(), path)
+ if err != nil {
+ return err
+ }
+ path = newPath
+ } else if parent != nil {
+ if strings.HasPrefix(path, "@") {
+ return parent.ErrTree(util.FmtNewtError(
+ "cannot import %s: no project loaded", path))
+ }
+ path = filepath.Join(filepath.Dir(parent.Path), path)
}
- path = newPath
}
// Only operate on absolute paths to ensure each file has a unique ID.
diff --git a/newt/config/config_test.go b/newt/config/config_test.go
new file mode 100644
index 00000000..1642a338
--- /dev/null
+++ b/newt/config/config_test.go
@@ -0,0 +1,87 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package config
+
+import (
+ "os"
+ "path/filepath"
+ "strings"
+ "testing"
+
+ "mynewt.apache.org/newt/newt/interfaces"
+)
+
+func configWriteFile(t *testing.T, path string, content string) {
+ t.Helper()
+ if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
+ t.Fatal(err)
+ }
+ if err := os.WriteFile(path, []byte(content), 0644); err != nil {
+ t.Fatal(err)
+ }
+}
+
+// Relative imports must not require a loaded project (e.g. newtrc is read
+// before the project). They are resolved relative to the importing file.
+func TestReadFileImportNoProject(t *testing.T) {
+ interfaces.SetProject(nil)
+
+ dir := t.TempDir()
+ top := filepath.Join(dir, "newtrc.yml")
+ configWriteFile(t, top, "$import:\n - sub/foo.yml\nkey1: top\n")
+ configWriteFile(t, filepath.Join(dir, "sub", "foo.yml"),
+ "$import:\n - bar.yml\nkey2: foo\n")
+ configWriteFile(t, filepath.Join(dir, "sub", "bar.yml"), "key3: bar\n")
+
+ yc, err := ReadFile(top)
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+
+ for k, exp := range map[string]string{
+ "key1": "top",
+ "key2": "foo",
+ "key3": "bar",
+ } {
+ v, err := yc.GetValString(k, nil)
+ if err != nil {
+ t.Fatalf("unexpected error reading %s: %v", k, err)
+ }
+ if v != exp {
+ t.Errorf("%s: expected %q, got %q", k, exp, v)
+ }
+ }
+}
+
+func TestReadFileRepoImportNoProject(t *testing.T) {
+ interfaces.SetProject(nil)
+
+ dir := t.TempDir()
+ top := filepath.Join(dir, "newtrc.yml")
+ configWriteFile(t, top, "$import:\n - \"@foo/bar.yml\"\n")
+
+ _, err := ReadFile(top)
+ if err == nil {
+ t.Fatalf("expected error")
+ }
+ if !strings.Contains(err.Error(), "no project") {
+ t.Fatalf("unexpected error: %v", err)
+ }
+}
(mynewt-newt) 11/30: newt/sysdown: Fix crashes on conflicting or invalid pkg.down
Posted by ja...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
sjanc pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/mynewt-newt.git
commit d50b2645563239e3bbb545d46fb6613b0629a852
Author: Szymon Janc <sz...@codecoup.pl>
AuthorDate: Mon Oct 19 17:49:47 2026 +0200
newt/sysdown: Fix crashes on conflicting or invalid pkg.down
The sysdown configuration did not initialize its conflicts map, so two
packages defining the same pkg.down function crashed newt with an
assignment to a nil map.
In addition, an invalid pkg.down entry was recorded as an error but a
zero-value stage function was still added. Two invalid entries then
collided on the empty function name, and commands printing the sysdown
configuration dereferenced the stage function's nil package.
Initialize the conflicts map and skip invalid entries, as sysinit
already does.
Assisted-by: Claude:claude-opus-5-5
---
newt/sysdown/sysdown.go | 5 ++-
newt/sysdown/sysdown_test.go | 89 ++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 93 insertions(+), 1 deletion(-)
diff --git a/newt/sysdown/sysdown.go b/newt/sysdown/sysdown.go
index 28bf6a13..d832a609 100644
--- a/newt/sysdown/sysdown.go
+++ b/newt/sysdown/sysdown.go
@@ -49,6 +49,7 @@ func (scfg *SysdownCfg) readOnePkg(lpkg *pkg.LocalPackage, cfg *syscfg.Cfg) {
sf, err := stage.NewStageFunc(name, stageStr, lpkg, cfg)
if err != nil {
scfg.InvalidSettings = append(scfg.InvalidSettings, err.Error())
+ continue
}
sf.ReturnType = "int"
sf.ArgList = "int reason"
@@ -74,7 +75,9 @@ func (scfg *SysdownCfg) detectConflicts() {
}
func Read(lpkgs []*pkg.LocalPackage, cfg *syscfg.Cfg) SysdownCfg {
- scfg := SysdownCfg{}
+ scfg := SysdownCfg{
+ Conflicts: map[string][]stage.StageFunc{},
+ }
for _, lpkg := range lpkgs {
scfg.readOnePkg(lpkg, cfg)
diff --git a/newt/sysdown/sysdown_test.go b/newt/sysdown/sysdown_test.go
new file mode 100644
index 00000000..424e3fb3
--- /dev/null
+++ b/newt/sysdown/sysdown_test.go
@@ -0,0 +1,89 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package sysdown
+
+import (
+ "testing"
+
+ "mynewt.apache.org/newt/newt/pkg"
+ "mynewt.apache.org/newt/newt/syscfg"
+)
+
+func sysdownPkg(t *testing.T, dir string, downs map[string]string) *pkg.LocalPackage {
+ t.Helper()
+
+ m := map[interface{}]interface{}{}
+ for name, stg := range downs {
+ m[name] = stg
+ }
+
+ lpkg := pkg.NewLocalPackage(nil, dir)
+ if err := lpkg.PkgY.Replace("pkg.down", m); err != nil {
+ t.Fatal(err)
+ }
+
+ return lpkg
+}
+
+// Two packages defining the same sysdown function are reported as a
+// conflict.
+func TestSysdownConflict(t *testing.T) {
+ cfg := syscfg.NewCfg()
+ lpkgs := []*pkg.LocalPackage{
+ sysdownPkg(t, "a", map[string]string{"foo_down": "100"}),
+ sysdownPkg(t, "b", map[string]string{"foo_down": "200"}),
+ }
+
+ scfg := Read(lpkgs, &cfg)
+
+ if len(scfg.Conflicts["foo_down"]) != 2 {
+ t.Errorf("expected conflict for foo_down, got %v", scfg.Conflicts)
+ }
+}
+
+// Invalid sysdown entries are reported and not added as stage functions.
+func TestSysdownInvalid(t *testing.T) {
+ cfg := syscfg.NewCfg()
+ lpkgs := []*pkg.LocalPackage{
+ sysdownPkg(t, "a", map[string]string{
+ "bad1_down": "xyz",
+ "bad2_down": "abc",
+ "ok_down": "100",
+ }),
+ }
+
+ scfg := Read(lpkgs, &cfg)
+
+ if len(scfg.InvalidSettings) != 2 {
+ t.Errorf("expected 2 invalid settings, got %v", scfg.InvalidSettings)
+ }
+ if len(scfg.Conflicts) != 0 {
+ t.Errorf("unexpected conflicts: %v", scfg.Conflicts)
+ }
+ if len(scfg.StageFuncs) != 1 || scfg.StageFuncs[0].Name != "ok_down" {
+ t.Errorf("expected only ok_down stage function, got %v",
+ scfg.StageFuncs)
+ }
+ for _, sf := range scfg.StageFuncs {
+ if sf.Pkg == nil {
+ t.Errorf("stage function %s without package", sf.Name)
+ }
+ }
+}
(mynewt-newt) 14/30: newt/syscfg: Fix nondeterministic chained value references
Posted by ja...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
sjanc pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/mynewt-newt.git
commit b3e37cf344f9cc19b132b9801f9b221163a7055e
Author: Szymon Janc <sz...@codecoup.pl>
AuthorDate: Mon Oct 19 17:54:27 2026 +0200
newt/syscfg: Fix nondeterministic chained value references
ResolveValueRefs replaced MYNEWT_VAL() references in a single pass over
the settings map. With chained references, e.g. A: MYNEWT_VAL(B) and
B: MYNEWT_VAL(C), the result depended on map iteration order: A ended
up either with the value of C or with the literal "MYNEWT_VAL(C)".
Resolve references recursively. References to undefined settings,
circular references (including a setting referencing itself) and
references to unresolvable settings are reported as unresolved.
Previously a self reference was silently left as a literal
MYNEWT_VAL() value. Unresolved references now also record the name of
the referenced setting, so the error lists it.
Assisted-by: Claude:claude-opus-5-5
---
newt/syscfg/syscfg.go | 61 +++++++++++++++++++++++++------
newt/syscfg/syscfg_test.go | 91 ++++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 141 insertions(+), 11 deletions(-)
diff --git a/newt/syscfg/syscfg.go b/newt/syscfg/syscfg.go
index 49da0bb0..337bebf2 100644
--- a/newt/syscfg/syscfg.go
+++ b/newt/syscfg/syscfg.go
@@ -270,21 +270,60 @@ func (cfg *Cfg) AddPkgNamesSettings(pkgs []*pkg.LocalPackage) {
}
}
+// Replaces values of the form `MYNEWT_VAL(<setting>)` with the value of the
+// referenced setting. Chained references are followed. References to
+// undefined settings and circular references are recorded as unresolved.
func (cfg *Cfg) ResolveValueRefs() {
- for k, entry := range cfg.Settings {
- refName, val, err := cfg.ExpandRef(strings.TrimSpace(entry.Value))
- if err != nil {
- // Referenced setting doesn't exist. Set unresolved setting value
- // to 0, this way restrictions can be evaluated and won't create
- // spurious warnings.
+ const (
+ refResolving = iota + 1
+ refDone
+ )
+
+ // setting-name => resolution state
+ states := map[string]int{}
+
+ // Resolves the specified setting; returns false if the setting contains
+ // an unresolvable reference.
+ var resolve func(k string) bool
+ resolve = func(k string) bool {
+ switch states[k] {
+ case refResolving:
+ // Circular reference.
+ return false
+ case refDone:
+ _, unresolved := cfg.UnresolvedValueRefs[k]
+ return !unresolved
+ }
+
+ states[k] = refResolving
+ defer func() { states[k] = refDone }()
+
+ entry := cfg.Settings[k]
+ refName := ResolveValueRefName(strings.TrimSpace(entry.Value))
+ if refName == "" {
+ // Not a reference.
+ return true
+ }
+
+ entry.ValueRefName = refName
+ _, ok := cfg.Settings[refName]
+ resolved := ok && resolve(refName)
+ if resolved {
+ entry.Value = cfg.Settings[refName].Value
+ } else {
+ // Referenced setting doesn't exist or can't be resolved. Set
+ // unresolved setting value to 0, this way restrictions can be
+ // evaluated and won't create spurious warnings.
entry.Value = "0"
cfg.UnresolvedValueRefs[k] = struct{}{}
- cfg.Settings[k] = entry
- } else if refName != "" {
- entry.ValueRefName = refName
- entry.Value = val
- cfg.Settings[k] = entry
}
+ cfg.Settings[k] = entry
+
+ return resolved
+ }
+
+ for k := range cfg.Settings {
+ resolve(k)
}
}
diff --git a/newt/syscfg/syscfg_test.go b/newt/syscfg/syscfg_test.go
new file mode 100644
index 00000000..0e64552d
--- /dev/null
+++ b/newt/syscfg/syscfg_test.go
@@ -0,0 +1,91 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package syscfg
+
+import (
+ "testing"
+)
+
+func valueRefsCfg(vals map[string]string) Cfg {
+ cfg := NewCfg()
+ for k, v := range vals {
+ cfg.Settings[k] = CfgEntry{Name: k, Value: v}
+ }
+
+ return cfg
+}
+
+// Chained references resolve to the final value regardless of map
+// iteration order.
+func TestResolveValueRefsChain(t *testing.T) {
+ // Map iteration order is random; repeat to cover different orders.
+ for i := 0; i < 50; i++ {
+ cfg := valueRefsCfg(map[string]string{
+ "A": "MYNEWT_VAL(B)",
+ "B": "MYNEWT_VAL(C)",
+ "C": "MYNEWT_VAL(D)",
+ "D": "5",
+ })
+ cfg.ResolveValueRefs()
+
+ for _, k := range []string{"A", "B", "C", "D"} {
+ if v := cfg.Settings[k].Value; v != "5" {
+ t.Fatalf("setting %s: got %q, want \"5\"", k, v)
+ }
+ }
+ if cfg.Settings["A"].ValueRefName != "B" {
+ t.Fatalf("setting A: got ref %q, want \"B\"",
+ cfg.Settings["A"].ValueRefName)
+ }
+ if len(cfg.UnresolvedValueRefs) != 0 {
+ t.Fatalf("unexpected unresolved refs: %v", cfg.UnresolvedValueRefs)
+ }
+ }
+}
+
+// Circular references and references to undefined settings are reported as
+// unresolved.
+func TestResolveValueRefsUnresolved(t *testing.T) {
+ for i := 0; i < 50; i++ {
+ cfg := valueRefsCfg(map[string]string{
+ "SELF": "MYNEWT_VAL(SELF)",
+ "MUT_A": "MYNEWT_VAL(MUT_B)",
+ "MUT_B": "MYNEWT_VAL(MUT_A)",
+ "UNDEF": "MYNEWT_VAL(NOT_DEFINED)",
+ "CHAIN": "MYNEWT_VAL(UNDEF)",
+ "OK": "1",
+ })
+ cfg.ResolveValueRefs()
+
+ for _, k := range []string{"SELF", "MUT_A", "MUT_B", "UNDEF",
+ "CHAIN"} {
+
+ if _, ok := cfg.UnresolvedValueRefs[k]; !ok {
+ t.Fatalf("setting %s not reported as unresolved", k)
+ }
+ if v := cfg.Settings[k].Value; v != "0" {
+ t.Fatalf("setting %s: got %q, want \"0\"", k, v)
+ }
+ }
+ if _, ok := cfg.UnresolvedValueRefs["OK"]; ok {
+ t.Fatalf("setting OK reported as unresolved")
+ }
+ }
+}
(mynewt-newt) 09/30: newt/syscfg: Fix crash on override of newt-injected settings
Posted by ja...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
sjanc pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/mynewt-newt.git
commit 96bf3cdd1cbe263923e249bd5e9aef1741ce8afa
Author: Szymon Janc <sz...@codecoup.pl>
AuthorDate: Mon Oct 19 17:48:25 2026 +0200
newt/syscfg: Fix crash on override of newt-injected settings
Settings injected by newt itself (TARGET_NAME, BSP_NAME, ARCH_NAME,
APP_NAME, NEWT_FEATURE_*, ...) have no defining package. Overriding
one of them from a package, e.g. TARGET_NAME in a target's syscfg.yml,
is reported as a priority violation, but generating the error text
dereferenced the nil defining package and newt crashed. The same
happened when dumping the configuration.
Report such an override as a setting that is set by newt and cannot be
overridden, and use "newt" as the package name in the dump.
Assisted-by: Claude:claude-opus-5-5
---
newt/dump/syscfg.go | 4 +-
newt/syscfg/syscfg.go | 40 +++++++++++++----
newt/syscfg_stability_test.go | 52 ++++++++++++++++++++++
newt/testdata/syscfg_stability/apps/app/pkg.yml | 24 ++++++++++
newt/testdata/syscfg_stability/project.yml | 25 +++++++++++
.../repos/apache-mynewt-core/compiler/sim/pkg.yml | 21 +++++++++
.../repos/apache-mynewt-core/hw/bsp/sim/bsp.yml | 28 ++++++++++++
.../repos/apache-mynewt-core/hw/bsp/sim/pkg.yml | 21 +++++++++
.../repos/apache-mynewt-core/repository.yml | 22 +++++++++
.../repos/apache-mynewt-core/sys/foo/pkg.yml | 24 ++++++++++
.../syscfg_stability/targets/newt_override/pkg.yml | 22 +++++++++
.../targets/newt_override/syscfg.yml | 22 +++++++++
.../targets/newt_override/target.yml | 23 ++++++++++
13 files changed, 318 insertions(+), 10 deletions(-)
diff --git a/newt/dump/syscfg.go b/newt/dump/syscfg.go
index 8b9205bf..172bf33c 100644
--- a/newt/dump/syscfg.go
+++ b/newt/dump/syscfg.go
@@ -124,8 +124,8 @@ func convStringMapRestrictionSlice(
func convPriority(p syscfg.CfgPriority) SyscfgPriority {
return SyscfgPriority{
Name: p.SettingName,
- Definer: p.PackageDef.FullName(),
- Overrider: p.PackageSrc.FullName(),
+ Definer: p.DefName(),
+ Overrider: p.SrcName(),
}
}
diff --git a/newt/syscfg/syscfg.go b/newt/syscfg/syscfg.go
index 3e1ed25e..a9d04fcf 100644
--- a/newt/syscfg/syscfg.go
+++ b/newt/syscfg/syscfg.go
@@ -113,6 +113,18 @@ type CfgPriority struct {
PackageSrc *pkg.LocalPackage // package overriding setting value.
}
+// Returns the name of the package that defines the setting, or "newt" if
+// the setting is injected by newt.
+func (p CfgPriority) DefName() string {
+ return lpkgNameOrNewt(p.PackageDef)
+}
+
+// Returns the name of the package that overrides the setting, or "newt" if
+// the value is injected by newt.
+func (p CfgPriority) SrcName() string {
+ return lpkgNameOrNewt(p.PackageSrc)
+}
+
type CfgFlashConflict struct {
SettingNames []string
Code CfgFlashConflictCode
@@ -308,12 +320,17 @@ func (cfg *Cfg) AllSettingsForLpkg(lpkg *pkg.LocalPackage) *cfgv.Settings {
return cfg.settingsForLpkg(lpkg, cfg.SettingValues())
}
-func (point CfgPoint) Name() string {
- if point.Source == nil {
+// Returns the full name of the package, or "newt" if the package is nil
+// (i.e., the setting or value is injected by newt).
+func lpkgNameOrNewt(lpkg *pkg.LocalPackage) string {
+ if lpkg == nil {
return "newt"
- } else {
- return point.Source.FullName()
}
+ return lpkg.FullName()
+}
+
+func (point CfgPoint) Name() string {
+ return lpkgNameOrNewt(point.Source)
}
func (point CfgPoint) IsInjected() bool {
@@ -1059,10 +1076,17 @@ func (cfg *Cfg) ErrorText() string {
entry := cfg.Settings[priority.SettingName]
historyMap[priority.SettingName] = entry.History
- str += fmt.Sprintf(
- " Package: %s overriding setting: %s defined by %s\n",
- priority.PackageSrc.FullName(), priority.SettingName,
- priority.PackageDef.FullName())
+ if priority.PackageDef == nil {
+ str += fmt.Sprintf(
+ " Package: %s overriding setting: %s which is set "+
+ "by newt and cannot be overridden\n",
+ priority.SrcName(), priority.SettingName)
+ } else {
+ str += fmt.Sprintf(
+ " Package: %s overriding setting: %s defined by %s\n",
+ priority.SrcName(), priority.SettingName,
+ priority.DefName())
+ }
}
}
diff --git a/newt/syscfg_stability_test.go b/newt/syscfg_stability_test.go
new file mode 100644
index 00000000..8a7addee
--- /dev/null
+++ b/newt/syscfg_stability_test.go
@@ -0,0 +1,52 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package main
+
+import (
+ "path/filepath"
+ "strings"
+ "testing"
+)
+
+var syscfgStabilityDir = filepath.Join("testdata", "syscfg_stability")
+
+// syscfgStabilityCheckNoPanic fails the test if newt output indicates a
+// crash.
+func syscfgStabilityCheckNoPanic(t *testing.T, out string) {
+ t.Helper()
+
+ if strings.Contains(out, "panic:") ||
+ strings.Contains(out, "goroutine ") {
+ t.Fatalf("newt crashed\n%s", out)
+ }
+}
+
+// Overriding a setting injected by newt (e.g., TARGET_NAME) is an error.
+func TestSyscfgStabilityNewtSettingOverride(t *testing.T) {
+ out, _ := newtCombinedOutput(syscfgStabilityDir, nil, "target",
+ "config", "show", "newt_override")
+ syscfgStabilityCheckNoPanic(t, out)
+
+ msg := "Package: targets/newt_override overriding setting: " +
+ "TARGET_NAME which is set by newt and cannot be overridden"
+ if !strings.Contains(out, msg) {
+ t.Errorf("missing priority violation error\n%s", out)
+ }
+}
diff --git a/newt/testdata/syscfg_stability/apps/app/pkg.yml b/newt/testdata/syscfg_stability/apps/app/pkg.yml
new file mode 100644
index 00000000..55bf224b
--- /dev/null
+++ b/newt/testdata/syscfg_stability/apps/app/pkg.yml
@@ -0,0 +1,24 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+#
+
+pkg.name: apps/app
+pkg.type: app
+
+pkg.deps:
+ - "@apache-mynewt-core/sys/foo"
diff --git a/newt/testdata/syscfg_stability/project.yml b/newt/testdata/syscfg_stability/project.yml
new file mode 100644
index 00000000..e67f5141
--- /dev/null
+++ b/newt/testdata/syscfg_stability/project.yml
@@ -0,0 +1,25 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+#
+
+project.name: "syscfg_stability"
+
+repository.apache-mynewt-core:
+ type: git
+ vers: 0.0.0
+ url: 'https://dummy.invalid/apache-mynewt-core.git'
diff --git a/newt/testdata/syscfg_stability/repos/apache-mynewt-core/compiler/sim/pkg.yml b/newt/testdata/syscfg_stability/repos/apache-mynewt-core/compiler/sim/pkg.yml
new file mode 100644
index 00000000..2f624582
--- /dev/null
+++ b/newt/testdata/syscfg_stability/repos/apache-mynewt-core/compiler/sim/pkg.yml
@@ -0,0 +1,21 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+#
+
+pkg.name: compiler/sim
+pkg.type: compiler
diff --git a/newt/testdata/syscfg_stability/repos/apache-mynewt-core/hw/bsp/sim/bsp.yml b/newt/testdata/syscfg_stability/repos/apache-mynewt-core/hw/bsp/sim/bsp.yml
new file mode 100644
index 00000000..934c920b
--- /dev/null
+++ b/newt/testdata/syscfg_stability/repos/apache-mynewt-core/hw/bsp/sim/bsp.yml
@@ -0,0 +1,28 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+#
+
+bsp.arch: sim
+bsp.compiler: "@apache-mynewt-core/compiler/sim"
+
+bsp.flash_map:
+ areas:
+ FLASH_AREA_IMAGE_0:
+ device: 0
+ offset: 0x00000000
+ size: 64kB
diff --git a/newt/testdata/syscfg_stability/repos/apache-mynewt-core/hw/bsp/sim/pkg.yml b/newt/testdata/syscfg_stability/repos/apache-mynewt-core/hw/bsp/sim/pkg.yml
new file mode 100644
index 00000000..7eee233c
--- /dev/null
+++ b/newt/testdata/syscfg_stability/repos/apache-mynewt-core/hw/bsp/sim/pkg.yml
@@ -0,0 +1,21 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+#
+
+pkg.name: hw/bsp/sim
+pkg.type: bsp
diff --git a/newt/testdata/syscfg_stability/repos/apache-mynewt-core/repository.yml b/newt/testdata/syscfg_stability/repos/apache-mynewt-core/repository.yml
new file mode 100644
index 00000000..aabf9b4b
--- /dev/null
+++ b/newt/testdata/syscfg_stability/repos/apache-mynewt-core/repository.yml
@@ -0,0 +1,22 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+#
+
+repo.name: apache-mynewt-core
+repo.versions:
+ "0.0.0": "master"
diff --git a/newt/testdata/syscfg_stability/repos/apache-mynewt-core/sys/foo/pkg.yml b/newt/testdata/syscfg_stability/repos/apache-mynewt-core/sys/foo/pkg.yml
new file mode 100644
index 00000000..5c17e91b
--- /dev/null
+++ b/newt/testdata/syscfg_stability/repos/apache-mynewt-core/sys/foo/pkg.yml
@@ -0,0 +1,24 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+#
+
+pkg.name: sys/foo
+pkg.type: lib
+
+pkg.init:
+ foo_init: 100
diff --git a/newt/testdata/syscfg_stability/targets/newt_override/pkg.yml b/newt/testdata/syscfg_stability/targets/newt_override/pkg.yml
new file mode 100644
index 00000000..60b49d3a
--- /dev/null
+++ b/newt/testdata/syscfg_stability/targets/newt_override/pkg.yml
@@ -0,0 +1,22 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+#
+
+
+pkg.name: targets/newt_override
+pkg.type: target
diff --git a/newt/testdata/syscfg_stability/targets/newt_override/syscfg.yml b/newt/testdata/syscfg_stability/targets/newt_override/syscfg.yml
new file mode 100644
index 00000000..8b503234
--- /dev/null
+++ b/newt/testdata/syscfg_stability/targets/newt_override/syscfg.yml
@@ -0,0 +1,22 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+#
+
+
+syscfg.vals:
+ TARGET_NAME: '"x"'
diff --git a/newt/testdata/syscfg_stability/targets/newt_override/target.yml b/newt/testdata/syscfg_stability/targets/newt_override/target.yml
new file mode 100644
index 00000000..7fbe84da
--- /dev/null
+++ b/newt/testdata/syscfg_stability/targets/newt_override/target.yml
@@ -0,0 +1,23 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+#
+
+
+target.app: apps/app
+target.bsp: "@apache-mynewt-core/hw/bsp/sim"
+target.build_profile: default
(mynewt-newt) 06/30: newt/downloader: Fix running git in project for non-git repo dirs
Posted by ja...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
sjanc pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/mynewt-newt.git
commit faff17fe74d10b3a9c3796c61aec9c36c5a2cabe
Author: Szymon Janc <sz...@codecoup.pl>
AuthorDate: Mon Oct 19 17:51:40 2026 +0200
newt/downloader: Fix running git in project for non-git repo dirs
Git commands for a repo were run by changing to its directory only.
If repos/<name> existed but was not a git checkout (e.g. a copied
tree) and the project itself was a git repository, git operated on
the project: newt upgrade rewrote the project's origin and checked
out the dependency commit in the project, removing project.yml.
Before running git in a directory, verify once per directory that it
is the top-level directory of a git repository and report an error
otherwise. The project directory itself is exempt, as a project may
be located inside a larger git repository.
Assisted-by: Claude:claude-opus-5-5
---
newt/downloader/downloader.go | 47 ++++++++++++++
newt/repo/repo.go | 3 +
newt/repogit_test.go | 109 ++++++++++++++++++++++++++++++++
newt/testdata/repogit_nogit/project.yml | 25 ++++++++
4 files changed, 184 insertions(+)
diff --git a/newt/downloader/downloader.go b/newt/downloader/downloader.go
index 9d647d1f..b96d7082 100644
--- a/newt/downloader/downloader.go
+++ b/newt/downloader/downloader.go
@@ -29,6 +29,7 @@ import (
"sort"
"strconv"
"strings"
+ "sync"
log "github.com/sirupsen/logrus"
@@ -176,6 +177,48 @@ func gitPath() (string, error) {
return filepath.ToSlash(gitPath), nil
}
+// Directories verified to be the top-level directory of a git repository, or
+// allowed not to be one.
+var gitTopLevelDirs = struct {
+ sync.Mutex
+ m map[string]struct{}
+}{m: map[string]struct{}{}}
+
+// AllowGitSubdir allows running git commands in the specified directory even
+// if it is not the top-level directory of a git repository (e.g., a project
+// located inside a larger git repository).
+func AllowGitSubdir(dir string) {
+ gitTopLevelDirs.Lock()
+ defer gitTopLevelDirs.Unlock()
+
+ gitTopLevelDirs.m[filepath.Clean(dir)] = struct{}{}
+}
+
+// checkGitTopLevel verifies that dir is the top-level directory of a git
+// repository. Otherwise git commands run in dir would operate on an
+// enclosing repository (e.g., the project) instead. Each directory is checked
+// only once. Must be called with dir as the current working directory.
+func checkGitTopLevel(gp string, dir string) error {
+ dir = filepath.Clean(dir)
+
+ gitTopLevelDirs.Lock()
+ defer gitTopLevelDirs.Unlock()
+
+ if _, ok := gitTopLevelDirs.m[dir]; ok {
+ return nil
+ }
+
+ out, err := exec.Command(gp, "rev-parse", "--show-cdup").Output()
+ if err != nil || strings.TrimSpace(string(out)) != "" {
+ return util.FmtNewtError(
+ "\"%s\" is not a git repository; remove it so that newt can "+
+ "download the repository again", filepath.ToSlash(dir))
+ }
+
+ gitTopLevelDirs.m[dir] = struct{}{}
+ return nil
+}
+
func executeGitCommand(dir string, cmd []string, logCmd bool) ([]byte, error) {
wd, err := os.Getwd()
if err != nil {
@@ -193,6 +236,10 @@ func executeGitCommand(dir string, cmd []string, logCmd bool) ([]byte, error) {
defer os.Chdir(wd)
+ if err := checkGitTopLevel(gp, dir); err != nil {
+ return nil, err
+ }
+
gitCmd := []string{gp}
gitCmd = append(gitCmd, cmd...)
output, err := util.ShellCommandLimitDbgOutput(gitCmd, nil, logCmd, -1)
diff --git a/newt/repo/repo.go b/newt/repo/repo.go
index 070cc452..16c126fc 100644
--- a/newt/repo/repo.go
+++ b/newt/repo/repo.go
@@ -688,5 +688,8 @@ func NewLocalRepo(repoName string) (*Repo, error) {
return nil, err
}
+ // The project may be located inside a larger git repository.
+ downloader.AllowGitSubdir(r.Path())
+
return r, nil
}
diff --git a/newt/repogit_test.go b/newt/repogit_test.go
new file mode 100644
index 00000000..d91c15e2
--- /dev/null
+++ b/newt/repogit_test.go
@@ -0,0 +1,109 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package main
+
+import (
+ "os"
+ "path/filepath"
+ "strings"
+ "testing"
+)
+
+var repoGitNoGitDir = filepath.Join("testdata", "repogit_nogit")
+
+// repoGitInitProject turns dir into a git repository with a single commit
+// and an "origin" remote. Returns the commit hash.
+func repoGitInitProject(t *testing.T, env []string, dir string,
+ origin string) string {
+
+ t.Helper()
+
+ env = append(env,
+ "GIT_AUTHOR_NAME=newt test", "GIT_AUTHOR_EMAIL=test@dummy.invalid",
+ "GIT_COMMITTER_NAME=newt test",
+ "GIT_COMMITTER_EMAIL=test@dummy.invalid")
+
+ runGit(t, env, nil, "-C", dir, "init", "-q")
+ runGit(t, env, nil, "-C", dir, "add", ".")
+ runGit(t, env, nil, "-C", dir, "commit", "-q", "-m", "project")
+ runGit(t, env, nil, "-C", dir, "remote", "add", "origin", origin)
+
+ return runGit(t, env, nil, "-C", dir, "rev-parse", "HEAD")
+}
+
+// If a directory in repos/ is not a git checkout, git commands for that repo
+// must not be run against the enclosing project git repository.
+func TestRepoGitNotCheckout(t *testing.T) {
+ env := dummyReposEnv(t)
+ projDir := copyToTemp(t, repoGitNoGitDir)
+
+ const origin = "https://example.invalid/project.git"
+ head := repoGitInitProject(t, env, projDir, origin)
+
+ newtOutput(t, projDir, env, "upgrade")
+
+ // Turn the installed repo into a plain copy of its files.
+ repoDir := filepath.Join(projDir, "repos", "mynewt-dummy-repo-04")
+ if err := os.Rename(filepath.Join(repoDir, ".git"),
+ filepath.Join(t.TempDir(), "git")); err != nil {
+
+ t.Fatal(err)
+ }
+
+ out, err := newtCombinedOutput(projDir, env, "upgrade")
+ if err == nil {
+ t.Fatalf("newt upgrade succeeded\n%s", out)
+ }
+ if !strings.Contains(out, "is not a git repository") {
+ t.Fatalf("unexpected error\n%s", out)
+ }
+
+ gotOrigin := runGit(t, env, nil, "-C", projDir, "config", "--get",
+ "remote.origin.url")
+ if gotOrigin != origin {
+ t.Fatalf("project origin changed to %q", gotOrigin)
+ }
+
+ gotHead := runGit(t, env, nil, "-C", projDir, "rev-parse", "HEAD")
+ if gotHead != head {
+ t.Fatalf("project HEAD changed to %s", gotHead)
+ }
+
+ if _, err := os.Stat(filepath.Join(projDir, "project.yml")); err != nil {
+ t.Fatalf("project.yml missing: %v", err)
+ }
+}
+
+// A project located in a subdirectory of a git repository must still work
+// and report the commit of that repository.
+func TestRepoGitProjectInSubdir(t *testing.T) {
+ env := dummyReposEnv(t)
+ projDir := copyToTemp(t, repoGitNoGitDir)
+
+ head := repoGitInitProject(t, env, filepath.Dir(projDir),
+ "https://example.invalid/project.git")
+
+ newtOutput(t, projDir, env, "upgrade")
+
+ out := newtOutput(t, projDir, env, "info")
+ if !strings.Contains(out, "repogit_nogit (project): "+head) {
+ t.Fatalf("project commit not reported\n%s", out)
+ }
+}
diff --git a/newt/testdata/repogit_nogit/project.yml b/newt/testdata/repogit_nogit/project.yml
new file mode 100644
index 00000000..d74881ee
--- /dev/null
+++ b/newt/testdata/repogit_nogit/project.yml
@@ -0,0 +1,25 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+#
+
+project.name: repogit_nogit
+
+repository.mynewt-dummy-repo-04:
+ type: git
+ vers: 0.0.0
+ url: 'https://dummy.invalid/mynewt-dummy-repo-04.git'
(mynewt-newt) 10/30: newt/syscfg: Fix crash on cmdline override of deprecated setting
Posted by ja...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
sjanc pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/mynewt-newt.git
commit 56bababf83d1f6f7b769adc28f8dcc22ce5df27a
Author: Szymon Janc <sz...@codecoup.pl>
AuthorDate: Mon Oct 19 17:48:47 2026 +0200
newt/syscfg: Fix crash on cmdline override of deprecated setting
Values injected with "-S" on the command line have no source package.
If such a value overrode a deprecated or experimental setting, newt
crashed while generating the warning because it dereferenced the nil
source package.
Use the history point name ("newt" for injected values) instead, as
the setting history output already does.
Assisted-by: Claude:claude-opus-5-5
---
newt/syscfg/syscfg.go | 4 +--
newt/syscfg_stability_test.go | 19 ++++++++++++++
.../repos/apache-mynewt-core/sys/foo/syscfg.yml | 29 ++++++++++++++++++++++
.../syscfg_stability/targets/deprecated/pkg.yml | 22 ++++++++++++++++
.../syscfg_stability/targets/deprecated/syscfg.yml | 23 +++++++++++++++++
.../syscfg_stability/targets/deprecated/target.yml | 23 +++++++++++++++++
6 files changed, 118 insertions(+), 2 deletions(-)
diff --git a/newt/syscfg/syscfg.go b/newt/syscfg/syscfg.go
index a9d04fcf..49da0bb0 100644
--- a/newt/syscfg/syscfg.go
+++ b/newt/syscfg/syscfg.go
@@ -1181,7 +1181,7 @@ func (cfg *Cfg) DeprecatedWarning() []string {
point := mostRecentPoint(entry)
lines = append(lines,
fmt.Sprintf("Use of deprecated setting %s in %s: %s", k,
- point.Source.FullName(), entry.Description))
+ point.Name(), entry.Description))
}
return lines
@@ -1200,7 +1200,7 @@ func (cfg *Cfg) ExperimentalWarning() []string {
point := mostRecentPoint(entry)
lines = append(lines,
fmt.Sprintf("Use of experimental setting %s in %s", k,
- point.Source.FullName()))
+ point.Name()))
}
return lines
diff --git a/newt/syscfg_stability_test.go b/newt/syscfg_stability_test.go
index 8a7addee..5b03e6e7 100644
--- a/newt/syscfg_stability_test.go
+++ b/newt/syscfg_stability_test.go
@@ -50,3 +50,22 @@ func TestSyscfgStabilityNewtSettingOverride(t *testing.T) {
t.Errorf("missing priority violation error\n%s", out)
}
}
+
+// Deprecated and experimental settings overridden on the command line
+// produce warnings naming newt as the source.
+func TestSyscfgStabilityCmdlineDeprecated(t *testing.T) {
+ dir := copyToTemp(t, syscfgStabilityDir)
+
+ out, _ := newtCombinedOutput(dir, nil, "build", "deprecated",
+ "-S", "FOO_DEPRECATED=2:FOO_EXPERIMENTAL=2")
+ syscfgStabilityCheckNoPanic(t, out)
+
+ for _, msg := range []string{
+ "Use of deprecated setting FOO_DEPRECATED in newt",
+ "Use of experimental setting FOO_EXPERIMENTAL in newt",
+ } {
+ if !strings.Contains(out, msg) {
+ t.Errorf("missing warning \"%s\"\n%s", msg, out)
+ }
+ }
+}
diff --git a/newt/testdata/syscfg_stability/repos/apache-mynewt-core/sys/foo/syscfg.yml b/newt/testdata/syscfg_stability/repos/apache-mynewt-core/sys/foo/syscfg.yml
new file mode 100644
index 00000000..0efb1fe5
--- /dev/null
+++ b/newt/testdata/syscfg_stability/repos/apache-mynewt-core/sys/foo/syscfg.yml
@@ -0,0 +1,29 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+#
+
+
+syscfg.defs:
+ FOO_DEPRECATED:
+ description: Deprecated setting.
+ value: 0
+ deprecated: 1
+ FOO_EXPERIMENTAL:
+ description: Experimental setting.
+ value: 0
+ experimental: 1
diff --git a/newt/testdata/syscfg_stability/targets/deprecated/pkg.yml b/newt/testdata/syscfg_stability/targets/deprecated/pkg.yml
new file mode 100644
index 00000000..cbfeb413
--- /dev/null
+++ b/newt/testdata/syscfg_stability/targets/deprecated/pkg.yml
@@ -0,0 +1,22 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+#
+
+
+pkg.name: targets/deprecated
+pkg.type: target
diff --git a/newt/testdata/syscfg_stability/targets/deprecated/syscfg.yml b/newt/testdata/syscfg_stability/targets/deprecated/syscfg.yml
new file mode 100644
index 00000000..d93315a2
--- /dev/null
+++ b/newt/testdata/syscfg_stability/targets/deprecated/syscfg.yml
@@ -0,0 +1,23 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+#
+
+
+syscfg.vals:
+ FOO_DEPRECATED: 1
+ FOO_EXPERIMENTAL: 1
diff --git a/newt/testdata/syscfg_stability/targets/deprecated/target.yml b/newt/testdata/syscfg_stability/targets/deprecated/target.yml
new file mode 100644
index 00000000..7fbe84da
--- /dev/null
+++ b/newt/testdata/syscfg_stability/targets/deprecated/target.yml
@@ -0,0 +1,23 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+#
+
+
+target.app: apps/app
+target.bsp: "@apache-mynewt-core/hw/bsp/sim"
+target.build_profile: default
(mynewt-newt) 27/30: newt/toolchain: Fix compiler warnings ending up in dependency files
Posted by ja...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
sjanc pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/mynewt-newt.git
commit 26d44628c291fdbe7cb6e58c39f83a38f0ef447c
Author: Szymon Janc <sz...@codecoup.pl>
AuthorDate: Mon Oct 19 17:53:11 2026 +0200
newt/toolchain: Fix compiler warnings ending up in dependency files
GenDepsForFile wrote the combined stdout and stderr of the compiler
to the .d file. Any warning printed while generating dependencies was
parsed as a dependency list, so the real dependencies were ignored or
nonexistent "dependencies" made every build recompile the file, or
parsing failed with "line missing ':'".
Only use the standard output for the dependency file. Standard error
is shown in verbose mode, or used as the error text if the command
fails.
Assisted-by: Claude:claude-opus-5-5
---
newt/toolchain/compiler.go | 34 +++++++++++++++++++++++++++++++++-
newt/toolchain/deps_test.go | 38 ++++++++++++++++++++++++++++++++++++++
2 files changed, 71 insertions(+), 1 deletion(-)
diff --git a/newt/toolchain/compiler.go b/newt/toolchain/compiler.go
index e8bc2df5..b8bf733d 100644
--- a/newt/toolchain/compiler.go
+++ b/newt/toolchain/compiler.go
@@ -20,6 +20,7 @@
package toolchain
import (
+ "bytes"
"fmt"
"io/ioutil"
"mynewt.apache.org/newt/newt/cfgv"
@@ -678,7 +679,7 @@ func (c *Compiler) GenDepsForFile(file string, compilerType int) error {
cmd = append(cmd, c.includesStrings()...)
cmd = append(cmd, []string{"-MM", "-MG", srcPath}...)
- o, err := util.ShellCommandLimitDbgOutput(cmd, nil, true, 0)
+ o, err := depsCommandOutput(cmd)
if err != nil {
return err
}
@@ -703,6 +704,37 @@ func (c *Compiler) GenDepsForFile(file string, compilerType int) error {
return nil
}
+// depsCommandOutput executes the specified dependency generation command and
+// returns its standard output. Standard error (e.g., compiler warnings) is
+// not part of the dependency list; it is only displayed in verbose mode or
+// returned as the error text if the command fails.
+func depsCommandOutput(cmdStrs []string) ([]byte, error) {
+ cmd, err := util.ShellCommandInit(cmdStrs, nil)
+ if err != nil {
+ return nil, err
+ }
+
+ util.LogShellCmd(cmdStrs, nil)
+
+ var stderr bytes.Buffer
+ cmd.Stderr = &stderr
+
+ o, err := cmd.Output()
+ if err != nil {
+ nerr := util.ChildNewtError(err)
+ if stderr.Len() > 0 {
+ nerr.Text = stderr.String()
+ }
+ return nil, nerr
+ }
+
+ if stderr.Len() > 0 {
+ util.StatusMessage(util.VERBOSITY_VERBOSE, "%s", stderr.String())
+ }
+
+ return o, nil
+}
+
func serializeCommand(cmd []string) []byte {
// Use a newline as the separator rather than a space to disambiguate cases
// where arguments contain spaces.
diff --git a/newt/toolchain/deps_test.go b/newt/toolchain/deps_test.go
index 585fcda5..eafc10b1 100644
--- a/newt/toolchain/deps_test.go
+++ b/newt/toolchain/deps_test.go
@@ -23,6 +23,8 @@ import (
"fmt"
"os"
"path/filepath"
+ "runtime"
+ "slices"
"sync"
"testing"
"time"
@@ -77,3 +79,39 @@ func TestCompilerConcurrentUpdates(t *testing.T) {
t.Errorf("expected %d compile commands, got %d", n, got)
}
}
+
+// Compiler warnings on stderr must not end up in the dependency file.
+func TestGenDepsForFileIgnoresStderr(t *testing.T) {
+ if runtime.GOOS == "windows" {
+ t.Skip("uses a shell script as a fake compiler")
+ }
+
+ dir := t.TempDir()
+ cc := filepath.Join(dir, "fake-cc")
+ script := "#!/bin/sh\n" +
+ "echo 'warning: something odd' >&2\n" +
+ "echo 'src/a.o: src/a.c src/a.h'\n"
+ if err := os.WriteFile(cc, []byte(script), 0755); err != nil {
+ t.Fatal(err)
+ }
+
+ c := newTestCompiler()
+ c.ccPath = cc
+ c.baseDir = dir
+ c.dstDir = dir + "/bin"
+ src := dir + "/src/a.c"
+
+ if err := c.GenDepsForFile(src, COMPILER_TYPE_C); err != nil {
+ t.Fatal(err)
+ }
+
+ deps, err := ParseDepsFile(c.dstFilePath(src) + ".d")
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ exp := []string{"src/a.c", "src/a.h"}
+ if !slices.Equal(deps, exp) {
+ t.Errorf("unexpected dependencies: expected %q, got %q", exp, deps)
+ }
+}
(mynewt-newt) 17/30: newt/cli: Fix panic in pkg copy/move with unknown source repo
Posted by ja...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
sjanc pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/mynewt-newt.git
commit 729d3e45255988060820f065273ef349d61cf2ee
Author: Szymon Janc <sz...@codecoup.pl>
AuthorDate: Mon Oct 19 17:50:18 2026 +0200
newt/cli: Fix panic in pkg copy/move with unknown source repo
"newt pkg copy @nosuch/foo apps/bar" (and "pkg move") passed a nil repo
returned by FindRepo to ResolvePackage, causing a nil pointer
dereference. Report that the source repo does not exist instead, as is
already done for the destination repo.
Assisted-by: Claude:claude-opus-5-5
---
newt/cli/pkg_cmds.go | 4 ++++
newt/cli_errors_test.go | 10 ++++++++++
2 files changed, 14 insertions(+)
diff --git a/newt/cli/pkg_cmds.go b/newt/cli/pkg_cmds.go
index 38248f9f..da7b3740 100644
--- a/newt/cli/pkg_cmds.go
+++ b/newt/cli/pkg_cmds.go
@@ -100,6 +100,10 @@ func pkgCloneOrMoveCmd(cmd *cobra.Command, args []string, dirOpFn dirOperation,
srcRepo := proj.LocalRepo()
if srcRepoName != "" {
srcRepo = proj.FindRepo(srcRepoName)
+ if srcRepo == nil {
+ NewtUsage(cmd, util.NewNewtError("Source repo "+
+ srcRepoName+" does not exist"))
+ }
}
srcPkg, err := proj.ResolvePackage(srcRepo, srcName)
diff --git a/newt/cli_errors_test.go b/newt/cli_errors_test.go
index 5b81baf2..2c1553df 100644
--- a/newt/cli_errors_test.go
+++ b/newt/cli_errors_test.go
@@ -52,3 +52,13 @@ func TestCliManNoArgs(t *testing.T) {
cliErrorCheck(t, projDir, nil, "Exactly one argument required",
"apropos")
}
+
+// "newt pkg copy" and "newt pkg move" with an unknown source repo.
+func TestCliPkgUnknownSrcRepo(t *testing.T) {
+ projDir := filepath.Join("testdata", "syscfg_collision")
+
+ for _, op := range []string{"copy", "move"} {
+ cliErrorCheck(t, projDir, nil, "Source repo nosuch does not exist",
+ "pkg", op, "@nosuch/foo", "apps/bar")
+ }
+}
(mynewt-newt) 28/30: newt/toolchain: Fix dependency files of sources with spaces in names
Posted by ja...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
sjanc pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/mynewt-newt.git
commit d4d477cf500c9ee12a711996d90597a681fd34a2
Author: Szymon Janc <sz...@codecoup.pl>
AuthorDate: Mon Oct 19 17:57:07 2026 +0200
newt/toolchain: Fix dependency files of sources with spaces in names
gcc escapes spaces in .d files ("my\ file.o: my\ file.c"), but newt
split dependency lines on white space without undoing the escaping,
and appended the object file name to the .d file unescaped. A source
file with a space in its name failed every build after the first one
with "line missing ':'".
Parse the Makefile escaping ("\ ", "\#", "$$") and escape the names
newt writes itself. If a dependency file still cannot be parsed (e.g.
one written by an older newt), remove it and rebuild the source
instead of failing the build.
Assisted-by: Claude:claude-opus-5-5
---
newt/toolchain/compiler.go | 9 +++-
newt/toolchain/deps.go | 66 +++++++++++++++++++++--
newt/toolchain/deps_test.go | 124 ++++++++++++++++++++++++++++++++++++++++++++
3 files changed, 194 insertions(+), 5 deletions(-)
diff --git a/newt/toolchain/compiler.go b/newt/toolchain/compiler.go
index b8bf733d..43b8e613 100644
--- a/newt/toolchain/compiler.go
+++ b/newt/toolchain/compiler.go
@@ -572,7 +572,11 @@ func (c *Compiler) lflagsStrings() []string {
}
func (c *Compiler) depsString() string {
- extraDeps := util.SortFields(c.extraDeps...)
+ extraDeps := util.UniqueStrings(c.extraDeps)
+ sort.Strings(extraDeps)
+ for i, d := range extraDeps {
+ extraDeps[i] = escapeDepsPath(d)
+ }
return strings.Join(extraDeps, " ") + "\n"
}
@@ -697,7 +701,8 @@ func (c *Compiler) GenDepsForFile(file string, compilerType int) error {
// Append the extra dependencies (.yml files) to the .d file.
objFile := strings.TrimSuffix(file, filepath.Ext(file)) + ".o"
- if _, err := f.WriteString(objFile + ": " + c.depsString()); err != nil {
+ if _, err := f.WriteString(escapeDepsPath(objFile) + ": " +
+ c.depsString()); err != nil {
return util.NewNewtError(err.Error())
}
diff --git a/newt/toolchain/deps.go b/newt/toolchain/deps.go
index 1da52934..2a14413e 100644
--- a/newt/toolchain/deps.go
+++ b/newt/toolchain/deps.go
@@ -66,17 +66,73 @@ func (d *DepTracker) setMostRecent(name string, t time.Time) {
d.MostRecentTime = time.Unix(t.Unix(), 0)
}
+// splitDepsTokens splits a line of a Makefile dependency file into white space
+// separated tokens. Escaping used in such files is undone: "\ " stands for a
+// space, "\#" for '#' and "$$" for '$'. Other backslashes are kept as is.
+func splitDepsTokens(line string) []string {
+ tokens := []string{}
+ var tok strings.Builder
+ inTok := false
+
+ for i := 0; i < len(line); i++ {
+ ch := line[i]
+ switch {
+ case ch == '\\' && i+1 < len(line) &&
+ strings.IndexByte(" \t#", line[i+1]) >= 0:
+
+ i++
+ tok.WriteByte(line[i])
+ inTok = true
+
+ case ch == '$' && i+1 < len(line) && line[i+1] == '$':
+ i++
+ tok.WriteByte('$')
+ inTok = true
+
+ case ch == ' ' || ch == '\t' || ch == '\r' || ch == '\n':
+ if inTok {
+ tokens = append(tokens, tok.String())
+ tok.Reset()
+ inTok = false
+ }
+
+ default:
+ tok.WriteByte(ch)
+ inTok = true
+ }
+ }
+
+ if inTok {
+ tokens = append(tokens, tok.String())
+ }
+
+ return tokens
+}
+
+// escapeDepsPath escapes a file name for use in a Makefile dependency file.
+// It is the inverse of the unescaping done by splitDepsTokens.
+func escapeDepsPath(path string) string {
+ r := strings.NewReplacer(
+ "$", "$$",
+ " ", "\\ ",
+ "\t", "\\\t",
+ "#", "\\#",
+ )
+
+ return r.Replace(path)
+}
+
// @return string The name of the dependent file (i.e., the first
// .o file encountered).
// @return []string Populated with the dependencies' filenames.
func parseDepsLine(line string) (string, []string, error) {
- tokens := strings.Fields(line)
+ tokens := splitDepsTokens(line)
if len(tokens) == 0 {
return "", nil, nil
}
dFileTok := tokens[0]
- if dFileTok[len(dFileTok)-1:] != ":" {
+ if !strings.HasSuffix(dFileTok, ":") {
return "", nil, util.NewNewtError("line missing ':'")
}
@@ -266,7 +322,11 @@ func (tracker *DepTracker) CompileRequired(srcFile string,
// Extract the dependency filenames from the dependency file.
deps, err := ParseDepsFile(depPath)
if err != nil {
- return false, err
+ // The dependency file is corrupt (e.g., generated by an older
+ // version of newt); delete it so it gets regenerated and rebuild.
+ logRebuildReqd(srcFile, err.Error())
+ os.Remove(depPath)
+ return true, nil
}
// Check if any dependencies are newer than the destination object file.
diff --git a/newt/toolchain/deps_test.go b/newt/toolchain/deps_test.go
index eafc10b1..f44fe3fa 100644
--- a/newt/toolchain/deps_test.go
+++ b/newt/toolchain/deps_test.go
@@ -115,3 +115,127 @@ func TestGenDepsForFileIgnoresStderr(t *testing.T) {
t.Errorf("unexpected dependencies: expected %q, got %q", exp, deps)
}
}
+
+func TestParseDepsFileEscaping(t *testing.T) {
+ dir := t.TempDir()
+ obj := dir + "/my dir/my file.o"
+
+ // As generated by gcc -MM for "my dir/my file.c", followed by the line
+ // newt appends itself.
+ content := "my\\ file.o: my\\ dir/my\\ file.c inc/a\\#b.h \\\n" +
+ " inc/c$$d.h \\\n" +
+ " inc/plain.h\n" +
+ escapeDepsPath(obj) + ": \n"
+
+ path := filepath.Join(dir, "my file.d")
+ if err := os.WriteFile(path, []byte(content), 0644); err != nil {
+ t.Fatal(err)
+ }
+
+ deps, err := ParseDepsFile(path)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ exp := []string{"my dir/my file.c", "inc/a#b.h", "inc/c$d.h",
+ "inc/plain.h"}
+ if !slices.Equal(deps, exp) {
+ t.Errorf("unexpected dependencies: expected %q, got %q", exp, deps)
+ }
+}
+
+func TestParseDepsLine(t *testing.T) {
+ tests := []struct {
+ line string
+ obj string
+ deps []string
+ err bool
+ }{
+ {"", "", nil, false},
+ {" ", "", nil, false},
+ {"a.o: a.c b.h", "a.o", []string{"a.c", "b.h"}, false},
+ {"a.o:", "a.o", []string{}, false},
+ {"C:/x/a.o: C:/x/a.c", "C:/x/a.o", []string{"C:/x/a.c"}, false},
+ {"a\\ b.o: a\\ b.c\tc.h\r", "a b.o", []string{"a b.c", "c.h"}, false},
+ {"a\\\\b.o: c\\d.h", "a\\\\b.o", []string{"c\\d.h"}, false},
+ {"a.c b.h", "", nil, true},
+ }
+
+ for _, tc := range tests {
+ obj, deps, err := parseDepsLine(tc.line)
+ if (err != nil) != tc.err {
+ t.Errorf("%q: unexpected error result: %v", tc.line, err)
+ continue
+ }
+ if obj != tc.obj || !slices.Equal(deps, tc.deps) {
+ t.Errorf("%q: expected %q %q, got %q %q", tc.line, tc.obj,
+ tc.deps, obj, deps)
+ }
+ }
+}
+
+func TestEscapeDepsPath(t *testing.T) {
+ for _, p := range []string{"plain/a.o", "/sp ace/a b.o", "a#b$c.o",
+ "tab\there.o"} {
+
+ tokens := splitDepsTokens(escapeDepsPath(p) + ": x")
+ if len(tokens) != 2 || tokens[0] != p+":" {
+ t.Errorf("%q: escape round trip failed: %q", p, tokens)
+ }
+ }
+}
+
+// A dependency file which cannot be parsed (e.g., written by an older newt)
+// must cause a rebuild rather than a permanent build failure.
+func TestCompileRequiredCorruptDepsFile(t *testing.T) {
+ dir := t.TempDir()
+ c := newTestCompiler()
+ c.ccPath = "cc"
+ c.baseDir = dir
+ c.dstDir = dir + "/bin"
+ src := dir + "/src/a.c"
+ dst := c.dstFilePath(src)
+
+ if err := os.MkdirAll(filepath.Dir(src), 0755); err != nil {
+ t.Fatal(err)
+ }
+ if err := os.MkdirAll(filepath.Dir(dst), 0755); err != nil {
+ t.Fatal(err)
+ }
+ if err := os.WriteFile(src, nil, 0644); err != nil {
+ t.Fatal(err)
+ }
+
+ cmd, err := c.CompileFileCmd(src, COMPILER_TYPE_C)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if err := writeCommandFile(dst+".o", cmd); err != nil {
+ t.Fatal(err)
+ }
+ if err := os.WriteFile(dst+".d", []byte("no colon here\n"),
+ 0644); err != nil {
+
+ t.Fatal(err)
+ }
+ if err := os.WriteFile(dst+".o", nil, 0644); err != nil {
+ t.Fatal(err)
+ }
+
+ // Make sure the source is older than the object and dependency files.
+ old := time.Now().Add(-time.Hour)
+ if err := os.Chtimes(src, old, old); err != nil {
+ t.Fatal(err)
+ }
+
+ required, err := c.depTracker.CompileRequired(src, COMPILER_TYPE_C)
+ if err != nil {
+ t.Fatalf("unexpected error: %s", err)
+ }
+ if !required {
+ t.Errorf("compile not required with corrupt dependency file")
+ }
+ if _, err := os.Stat(dst + ".d"); !os.IsNotExist(err) {
+ t.Errorf("corrupt dependency file not removed")
+ }
+}
(mynewt-newt) 26/30: newt/toolchain: Fix data races during parallel compilation
Posted by ja...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
sjanc pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/mynewt-newt.git
commit a47b0b2a43c8b9d71ce834fb0cbb26edb79cecca
Author: Szymon Janc <sz...@codecoup.pl>
AuthorDate: Mon Oct 19 17:52:35 2026 +0200
newt/toolchain: Fix data races during parallel compilation
With -j > 1 the files of one package are compiled concurrently, but
the list of compile commands and the dependency tracker's most recent
object name/time were updated without locking. This could lose
entries in compile_commands.json and corrupt the timestamp used to
decide whether to rearchive and relink.
Protect these with the compiler mutex, which already guards the
object path list, and return a copy of the compile commands.
Assisted-by: Claude:claude-opus-5-5
---
newt/toolchain/compiler.go | 25 ++++++++++----
newt/toolchain/deps.go | 13 +++++++-
newt/toolchain/deps_test.go | 79 +++++++++++++++++++++++++++++++++++++++++++++
3 files changed, 109 insertions(+), 8 deletions(-)
diff --git a/newt/toolchain/compiler.go b/newt/toolchain/compiler.go
index 651dfba0..e8bc2df5 100644
--- a/newt/toolchain/compiler.go
+++ b/newt/toolchain/compiler.go
@@ -77,7 +77,8 @@ type Compiler struct {
AutogeneratedLinkerIncludeDir string
// Needs to be locked whenever a mutable field in this struct is accessed
- // during a build. Currently, objPathList is the only such member.
+ // during a build: objPathList, compileCommands and the dependency
+ // tracker's most recent file data.
mutex *sync.Mutex
depTracker DepTracker
@@ -119,7 +120,21 @@ type Compiler struct {
}
func (c *Compiler) GetCompileCommands() []CompileCommand {
- return c.compileCommands
+ c.mutex.Lock()
+ defer c.mutex.Unlock()
+
+ return append([]CompileCommand{}, c.compileCommands...)
+}
+
+func (c *Compiler) addCompileCommand(cmd []string, file string) {
+ c.mutex.Lock()
+ defer c.mutex.Unlock()
+
+ c.compileCommands = append(c.compileCommands,
+ CompileCommand{
+ Command: strings.Join(cmd, " "),
+ File: file,
+ })
}
func (c *Compiler) GetCcPath() string {
@@ -761,11 +776,7 @@ func (c *Compiler) CompileFile(file string, compilerType int) error {
}
util.StatusMessage(util.VERBOSITY_DEFAULT, "%s", string(o))
- c.compileCommands = append(c.compileCommands,
- CompileCommand{
- Command: strings.Join(cmd, " "),
- File: file,
- })
+ c.addCompileCommand(cmd, file)
err = writeCommandFile(objPath, cmd)
if err != nil {
diff --git a/newt/toolchain/deps.go b/newt/toolchain/deps.go
index fa526f7a..1da52934 100644
--- a/newt/toolchain/deps.go
+++ b/newt/toolchain/deps.go
@@ -50,6 +50,14 @@ func NewDepTracker(c *Compiler) DepTracker {
}
func (d *DepTracker) SetMostRecent(name string, t time.Time) {
+ d.compiler.mutex.Lock()
+ defer d.compiler.mutex.Unlock()
+
+ d.setMostRecent(name, t)
+}
+
+// setMostRecent must be called with the compiler mutex locked.
+func (d *DepTracker) setMostRecent(name string, t time.Time) {
d.MostRecentName = name
// Truncate sub-second part of timestamp. Timestamps of generated files
@@ -128,8 +136,11 @@ func (tracker *DepTracker) ProcessFileTime(file string) error {
return err
}
+ tracker.compiler.mutex.Lock()
+ defer tracker.compiler.mutex.Unlock()
+
if modTime.After(tracker.MostRecentTime) {
- tracker.SetMostRecent(file, modTime)
+ tracker.setMostRecent(file, modTime)
}
return nil
diff --git a/newt/toolchain/deps_test.go b/newt/toolchain/deps_test.go
new file mode 100644
index 00000000..585fcda5
--- /dev/null
+++ b/newt/toolchain/deps_test.go
@@ -0,0 +1,79 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package toolchain
+
+import (
+ "fmt"
+ "os"
+ "path/filepath"
+ "sync"
+ "testing"
+ "time"
+)
+
+// newTestCompiler creates a compiler which is usable for tests that do not
+// need a loaded compiler package.
+func newTestCompiler() *Compiler {
+ c := &Compiler{
+ mutex: &sync.Mutex{},
+ objPathList: map[string]bool{},
+ extraDeps: []string{},
+ compileCommands: []CompileCommand{},
+ }
+ c.depTracker = NewDepTracker(c)
+
+ return c
+}
+
+// Run with -race to detect unsynchronized access.
+func TestCompilerConcurrentUpdates(t *testing.T) {
+ c := newTestCompiler()
+ dir := t.TempDir()
+
+ const n = 50
+ var wg sync.WaitGroup
+ for i := 0; i < n; i++ {
+ obj := filepath.Join(dir, fmt.Sprintf("f%d.o", i))
+ if err := os.WriteFile(obj, nil, 0644); err != nil {
+ t.Fatal(err)
+ }
+
+ wg.Add(3)
+ go func() {
+ defer wg.Done()
+ c.addCompileCommand([]string{"cc", "-c", obj}, obj)
+ }()
+ go func() {
+ defer wg.Done()
+ c.depTracker.SetMostRecent(obj, time.Now())
+ }()
+ go func() {
+ defer wg.Done()
+ if err := c.depTracker.ProcessFileTime(obj); err != nil {
+ t.Error(err)
+ }
+ }()
+ }
+ wg.Wait()
+
+ if got := len(c.GetCompileCommands()); got != n {
+ t.Errorf("expected %d compile commands, got %d", n, got)
+ }
+}
(mynewt-newt) 03/30: newt/compat: Fix panic on duplicate version in compatibility table
Posted by ja...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
sjanc pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/mynewt-newt.git
commit 26ab97a7f5d9c907dbc6d5cb19622b7498a6491c
Author: Szymon Janc <sz...@codecoup.pl>
AuthorDate: Mon Oct 19 17:47:10 2026 +0200
newt/compat: Fix panic on duplicate version in compatibility table
A newt_compatibility table could contain the same newt version twice
using different spellings, e.g. "1.0.0: good" and "1.0.00: error".
Depending on map iteration order the good range was then computed
reversed and CheckNewtVer indexed the table at -1.
Reject tables with duplicate newt versions with an error.
Assisted-by: Claude:claude-opus-5-5
---
newt/compat/compat.go | 8 +++++
newt/compat/compat_test.go | 81 ++++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 89 insertions(+)
diff --git a/newt/compat/compat.go b/newt/compat/compat.go
index 4034ff66..5d7bcb34 100644
--- a/newt/compat/compat.go
+++ b/newt/compat/compat.go
@@ -101,6 +101,14 @@ func ParseNcTable(strMap map[string]string) (NewtCompatTable, error) {
sortEntries(tbl)
+ for i := 1; i < len(tbl); i++ {
+ if newtutil.VerCmp(tbl[i-1].minNewtVer, tbl[i].minNewtVer) == 0 {
+ return nil, util.FmtNewtError("Newt compatibility table "+
+ "contains duplicate newt version: %s",
+ tbl[i].minNewtVer.String())
+ }
+ }
+
return tbl, nil
}
diff --git a/newt/compat/compat_test.go b/newt/compat/compat_test.go
new file mode 100644
index 00000000..a1811a48
--- /dev/null
+++ b/newt/compat/compat_test.go
@@ -0,0 +1,81 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package compat
+
+import (
+ "testing"
+
+ "mynewt.apache.org/newt/newt/newtutil"
+)
+
+func compatTestVer(t *testing.T, s string) newtutil.Version {
+ t.Helper()
+ v, err := newtutil.ParseVersion(s)
+ if err != nil {
+ t.Fatalf("failed to parse version %q: %v", s, err)
+ }
+ return v
+}
+
+func TestParseNcTableDuplicateVersion(t *testing.T) {
+ strMap := map[string]string{
+ "1.0.0": "good",
+ "1.0.00": "error",
+ }
+
+ // The order of map iteration is random; repeat to cover both orders
+ // of the duplicate entries.
+ for i := 0; i < 20; i++ {
+ tbl, err := ParseNcTable(strMap)
+ if err == nil {
+ // Without duplicate detection this could panic.
+ tbl.CheckNewtVer(compatTestVer(t, "0.9.0"))
+ t.Fatalf("expected error for duplicate newt version")
+ }
+ }
+}
+
+func TestCheckNewtVer(t *testing.T) {
+ tbl, err := ParseNcTable(map[string]string{
+ "1.0.0": "good",
+ "2.0.0": "error",
+ })
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+
+ tests := []struct {
+ ver newtutil.Version
+ code NewtCompatCode
+ }{
+ {compatTestVer(t, "0.9.0"), NEWT_COMPAT_ERROR},
+ {compatTestVer(t, "1.0.0"), NEWT_COMPAT_GOOD},
+ {compatTestVer(t, "1.5.0"), NEWT_COMPAT_GOOD},
+ {compatTestVer(t, "2.0.0"), NEWT_COMPAT_ERROR},
+ }
+
+ for _, tc := range tests {
+ code, _ := tbl.CheckNewtVer(tc.ver)
+ if code != tc.code {
+ t.Errorf("newt %s: expected code %d, got %d",
+ tc.ver.String(), tc.code, code)
+ }
+ }
+}
(mynewt-newt) 16/30: newt/cli: Fix panic in man and apropos without argument
Posted by ja...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
sjanc pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/mynewt-newt.git
commit 8aaefc8a6025cb57987308182c0461e999a04c97
Author: Szymon Janc <sz...@codecoup.pl>
AuthorDate: Mon Oct 19 17:50:00 2026 +0200
newt/cli: Fix panic in man and apropos without argument
"newt man" and "newt apropos" used args[0] without checking that an
argument was given, causing an index out of range panic. Print usage
with an error instead.
Assisted-by: Claude:claude-opus-5-5
---
newt/cli/man_cmds.go | 9 +++++++++
newt/cli_errors_test.go | 54 +++++++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 63 insertions(+)
diff --git a/newt/cli/man_cmds.go b/newt/cli/man_cmds.go
index eb59c310..6160f13a 100644
--- a/newt/cli/man_cmds.go
+++ b/newt/cli/man_cmds.go
@@ -22,6 +22,7 @@ package cli
import (
"github.com/spf13/cobra"
"mynewt.apache.org/newt/newt/man"
+ "mynewt.apache.org/newt/util"
)
func manBuildRunCmd(cmd *cobra.Command, args []string) {
@@ -33,6 +34,10 @@ func manBuildRunCmd(cmd *cobra.Command, args []string) {
}
func manRunCmd(cmd *cobra.Command, args []string) {
+ if len(args) != 1 {
+ NewtUsage(cmd, util.NewNewtError("Exactly one argument required"))
+ }
+
proj := TryGetProject()
err := man.RunMan(proj, args)
if err != nil {
@@ -41,6 +46,10 @@ func manRunCmd(cmd *cobra.Command, args []string) {
}
func aproposRunCmd(cmd *cobra.Command, args []string) {
+ if len(args) != 1 {
+ NewtUsage(cmd, util.NewNewtError("Exactly one argument required"))
+ }
+
proj := TryGetProject()
err := man.RunApropos(proj, args)
if err != nil {
diff --git a/newt/cli_errors_test.go b/newt/cli_errors_test.go
new file mode 100644
index 00000000..5b81baf2
--- /dev/null
+++ b/newt/cli_errors_test.go
@@ -0,0 +1,54 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package main
+
+import (
+ "path/filepath"
+ "strings"
+ "testing"
+)
+
+// cliErrorCheck runs newt and verifies that it fails gracefully, i.e.,
+// with an error message containing msg rather than with a panic.
+func cliErrorCheck(t *testing.T, dir string, env []string, msg string,
+ args ...string) {
+
+ t.Helper()
+
+ out, err := newtCombinedOutput(dir, env, args...)
+ if err == nil {
+ t.Errorf("newt %q succeeded\n%s", args, out)
+ }
+ if strings.Contains(out, "panic") || strings.Contains(out, "goroutine ") {
+ t.Errorf("newt %q panicked\n%s", args, out)
+ }
+ if !strings.Contains(out, msg) {
+ t.Errorf("newt %q: missing %q in output\n%s", args, msg, out)
+ }
+}
+
+// "newt man" and "newt apropos" require an argument.
+func TestCliManNoArgs(t *testing.T) {
+ projDir := filepath.Join("testdata", "syscfg_collision")
+
+ cliErrorCheck(t, projDir, nil, "Exactly one argument required", "man")
+ cliErrorCheck(t, projDir, nil, "Exactly one argument required",
+ "apropos")
+}
(mynewt-newt) 07/30: newt/parse: Fix crash on empty parentheses in expressions
Posted by ja...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
sjanc pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/mynewt-newt.git
commit 3a7a7e58bb4718257c518017b9e828aea60adda4
Author: Szymon Janc <sz...@codecoup.pl>
AuthorDate: Mon Oct 19 17:45:44 2026 +0200
newt/parse: Fix crash on empty parentheses in expressions
An empty parenthesized group, e.g. "X == ()" or "() < 1", was parsed
into a nil operand node. Evaluating such a binary operator later
dereferenced the nil node and newt crashed with a nil pointer panic.
Treat an empty parenthesized group as a parse error.
Assisted-by: Claude:claude-opus-5-5
---
newt/parse/parse.go | 3 ++
newt/parse/parse_test.go | 76 ++++++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 79 insertions(+)
diff --git a/newt/parse/parse.go b/newt/parse/parse.go
index 1d474ec3..05ed0e49 100644
--- a/newt/parse/parse.go
+++ b/newt/parse/parse.go
@@ -304,6 +304,9 @@ func Parse(tokens []Token) (*Node, error) {
return nil, fmt.Errorf("unexpected %s after parenthesis",
tokens[len(stripped)+2].Text)
}
+ if len(stripped) == 0 {
+ return nil, fmt.Errorf("empty parentheses")
+ }
return Parse(stripped)
}
diff --git a/newt/parse/parse_test.go b/newt/parse/parse_test.go
new file mode 100644
index 00000000..0413e21e
--- /dev/null
+++ b/newt/parse/parse_test.go
@@ -0,0 +1,76 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package parse
+
+import (
+ "testing"
+
+ "mynewt.apache.org/newt/newt/cfgv"
+)
+
+func TestParseEmptyParens(t *testing.T) {
+ settings := cfgv.NewSettingsFromMap(map[string]string{"X": "1"})
+
+ exprs := []string{
+ "()",
+ "X == ()",
+ "() == 1",
+ "X != ()",
+ "X < ()",
+ "() >= X",
+ "X && ()",
+ "!()",
+ "(())",
+ "X == (())",
+ }
+ for _, expr := range exprs {
+ _, err := ParseAndEval(expr, settings)
+ if err == nil {
+ t.Errorf("expected error for expression [%s]", expr)
+ }
+ }
+}
+
+func TestParseValid(t *testing.T) {
+ settings := cfgv.NewSettingsFromMap(map[string]string{
+ "X": "1",
+ "Y": "0",
+ })
+
+ exprs := map[string]bool{
+ "": true,
+ "X": true,
+ "(X)": true,
+ "((X) == (1))": true,
+ "X == (Y)": false,
+ "!(Y)": true,
+ "X > Y": true,
+ }
+ for expr, want := range exprs {
+ got, err := ParseAndEval(expr, settings)
+ if err != nil {
+ t.Errorf("unexpected error for expression [%s]: %v", expr, err)
+ continue
+ }
+ if got != want {
+ t.Errorf("expression [%s]: got %v, want %v", expr, got, want)
+ }
+ }
+}
(mynewt-newt) 20/30: newt/mfg: Fix panic on target without app or BSP
Posted by ja...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
sjanc pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/mynewt-newt.git
commit a5f5663a30ac1bce65330e5ac37e3704e73dc4dc
Author: Szymon Janc <sz...@codecoup.pl>
AuthorDate: Mon Oct 19 17:52:43 2026 +0200
newt/mfg: Fix panic on target without app or BSP
If a target referenced in mfg.yml had no app or BSP, or the package
could not be resolved, newt dereferenced a nil package, either when
looking up the app manifest or in NewBspPackage. Report an error naming
the target instead.
Assisted-by: Claude:claude-opus-5-5
---
newt/mfg/build.go | 14 ++++++-
newt/mfg_errors_test.go | 107 ++++++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 120 insertions(+), 1 deletion(-)
diff --git a/newt/mfg/build.go b/newt/mfg/build.go
index e11efbfc..648dc0d5 100644
--- a/newt/mfg/build.go
+++ b/newt/mfg/build.go
@@ -140,6 +140,11 @@ func calcBsp(dm DecodedMfg,
}
bspLpkg = t.Bsp()
+ if bspLpkg == nil {
+ return nil, util.FmtNewtError(
+ "target \"%s\" has missing or unresolvable BSP \"%s\"",
+ dt.Name, t.BspName)
+ }
bspMap[bspLpkg] = struct{}{}
}
@@ -220,7 +225,14 @@ func newMfgBuildTarget(dt DecodedTarget,
return MfgBuildTarget{}, err
}
- mpath := builder.ManifestPath(dt.Name, builder.BUILD_NAME_APP, t.App().FullName())
+ app := t.App()
+ if app == nil {
+ return MfgBuildTarget{}, util.FmtNewtError(
+ "target \"%s\" has missing or unresolvable app \"%s\"",
+ dt.Name, t.AppName)
+ }
+
+ mpath := builder.ManifestPath(dt.Name, builder.BUILD_NAME_APP, app.FullName())
man, err := manifest.ReadManifest(mpath)
if err != nil {
return MfgBuildTarget{}, util.FmtNewtError("%s", err.Error())
diff --git a/newt/mfg_errors_test.go b/newt/mfg_errors_test.go
new file mode 100644
index 00000000..9fcefd12
--- /dev/null
+++ b/newt/mfg_errors_test.go
@@ -0,0 +1,107 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package main
+
+import (
+ "os"
+ "path/filepath"
+ "testing"
+)
+
+// mfgErrorsWrite writes a file with the given contents into the project,
+// creating its parent directories as needed.
+func mfgErrorsWrite(t *testing.T, projDir string, path string,
+ contents string) {
+
+ t.Helper()
+
+ path = filepath.Join(projDir, path)
+ if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
+ t.Fatal(err)
+ }
+ if err := os.WriteFile(path, []byte(contents), 0644); err != nil {
+ t.Fatal(err)
+ }
+}
+
+// mfgErrorsProject creates a project with a target named "targets/mfgtgt"
+// using the given target.yml contents and an mfg package "mfgs/mfgtest"
+// referencing this target.
+func mfgErrorsProject(t *testing.T, targetYml string) string {
+ t.Helper()
+
+ projDir := copyToTemp(t, filepath.Join("testdata", "syscfg_collision"))
+
+ mfgErrorsWrite(t, projDir, "targets/mfgtgt/pkg.yml",
+ "pkg.name: targets/mfgtgt\npkg.type: target\n")
+ mfgErrorsWrite(t, projDir, "targets/mfgtgt/target.yml", targetYml)
+ mfgErrorsWrite(t, projDir, "mfgs/mfgtest/pkg.yml",
+ "pkg.name: mfgs/mfgtest\npkg.type: mfg\n")
+ mfgErrorsWrite(t, projDir, "mfgs/mfgtest/mfg.yml", `
+mfg.targets:
+ - name: targets/mfgtgt
+ area: FLASH_AREA_IMAGE_0
+ offset: 0
+mfg.meta:
+ area: FLASH_AREA_IMAGE_0
+`)
+
+ return projDir
+}
+
+// Targets referenced by mfg.yml without a valid app or BSP must be
+// reported as errors.
+func TestMfgErrorsTargetPkgs(t *testing.T) {
+ for _, tc := range []struct {
+ name string
+ targetYml string
+ msg string
+ }{
+ {
+ name: "no_bsp",
+ targetYml: "target.app: apps/app\n",
+ msg: `target "targets/mfgtgt" has missing or unresolvable BSP`,
+ },
+ {
+ name: "bad_bsp",
+ targetYml: "target.app: apps/app\n" +
+ "target.bsp: \"@apache-mynewt-core/hw/bsp/nosuch\"\n",
+ msg: `target "targets/mfgtgt" has missing or unresolvable BSP`,
+ },
+ {
+ name: "no_app",
+ targetYml: "target.bsp: \"@apache-mynewt-core/hw/bsp/sim\"\n",
+ msg: `target "targets/mfgtgt" has missing or unresolvable app`,
+ },
+ {
+ name: "bad_app",
+ targetYml: "target.app: apps/nosuch\n" +
+ "target.bsp: \"@apache-mynewt-core/hw/bsp/sim\"\n",
+ msg: `target "targets/mfgtgt" has missing or unresolvable app`,
+ },
+ } {
+ t.Run(tc.name, func(t *testing.T) {
+ projDir := mfgErrorsProject(t, tc.targetYml)
+
+ cliErrorCheck(t, projDir, nil, tc.msg,
+ "mfg", "create", "mfgtest", "1.0.0")
+ })
+ }
+}