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")
+		})
+	}
+}