From bebde7fab7f8abe8da8ef0247e4b55d54ae29e1a Mon Sep 17 00:00:00 2001 From: Sameeh Jubran Date: Wed, 28 Oct 2026 14:13:44 +0200 Subject: [PATCH 1/2] fix(sbom.mk): start recipe lines without a literal tab The wolfSSL source-text check rejects hard tabs in *.mk files. A product that vendors share/ (wolfSSL PR 11701) fails that check on tools/sbom/build/sbom.mk. Start each recipe line in wolfssl_compliance_tooling_sbom_rule with $(SBOM_TAB). $(call) expands it to a tab before $(eval) parses the rule, so make reads the same recipe. Indent the continuation lines with spaces. The driver arguments and the error checks do not change (checked with GNU make 3.81). --- share/build/sbom.mk | 168 +++++++++++++++++++++++--------------------- 1 file changed, 86 insertions(+), 82 deletions(-) diff --git a/share/build/sbom.mk b/share/build/sbom.mk index 79882d4..3a6d3e2 100644 --- a/share/build/sbom.mk +++ b/share/build/sbom.mk @@ -133,91 +133,95 @@ SBOM_ROOT ?= $(CURDIR) SBOM_LICENSE_FILE ?= $(SBOM_ROOT)/LICENSE HOSTCC ?= cc +# Recipe lines in the rule below start with $(SBOM_TAB), not a literal tab. +# $(call) expands it before $(eval) parses the rule, so make still sees a tab. +SBOM_TAB := $(shell printf '\t') + define wolfssl_compliance_tooling_sbom_rule .PHONY: $(1) $(1): $($(2)PREREQS) - @test -n "$($(2)NAME)" || { echo "ERROR: set $(2)NAME"; exit 1; } - @if [ "$($(2)NO_ARTIFACT_HASH)" = "1" ]; then \ - test -z "$(strip $($(2)SRCS))$($(2)LIB)" || \ - { echo "ERROR: $(2)NO_ARTIFACT_HASH stands alone. Do not set $(2)SRCS or $(2)LIB."; exit 1; }; \ - else \ - test -n "$(strip $($(2)SRCS))$($(2)LIB)" || \ - { echo "ERROR: set $(2)SRCS or $(2)LIB"; exit 1; }; \ - fi - @set -e; \ - if [ -n "$(strip $($(2)SRCS))" ]; then \ - trap 'rm -f "$(CURDIR)/.$(1)-wolfssl-compliance-tooling-srcs.txt"' EXIT INT TERM HUP; \ - printf '%s\n' $($(2)SRCS) > "$(CURDIR)/.$(1)-wolfssl-compliance-tooling-srcs.txt"; \ - fi; \ - dep_ver=""; \ - if [ -n "$($(2)DEP_VERSION)" ]; then \ - for dv in $($(2)DEP_VERSION); do \ - dep_ver="$$$$dep_ver --dep-version $$$$dv"; \ - done; \ - fi; \ - if [ "$($(2)DEP_WOLFSSL)" = "yes" ] || [ "$($(2)DEP_WOLFSSL)" = "1" ]; then \ - wv="$($(2)WOLFSSL_VERSION)"; \ - if [ -z "$$$$wv" ] && [ -n "$(WOLFSSL_DIR)" ] && \ - [ -f "$(WOLFSSL_DIR)/wolfssl/version.h" ]; then \ - wv=`sed -n 's/.*LIBWOLFSSL_VERSION_STRING[[:space:]]*"\([^"]*\)".*/\1/p' \ - "$(WOLFSSL_DIR)/wolfssl/version.h" | head -1`; \ - fi; \ - if [ -n "$$$$wv" ]; then \ - dep_ver="$$$$dep_ver --dep-version wolfssl=$$$$wv"; \ - fi; \ - fi; \ - if [ "$($(2)DEP_WOLFCRYPT)" = "yes" ] || [ "$($(2)DEP_WOLFCRYPT)" = "1" ]; then \ - case " $$$$dep_ver " in \ - *" --dep-version wolfcrypt="*) ;; \ - *) \ - cv="$($(2)WOLFSSL_VERSION)"; \ - if [ -z "$$$$cv" ] && [ -n "$(WOLFSSL_DIR)" ] && \ - [ -f "$(WOLFSSL_DIR)/wolfssl/version.h" ]; then \ - cv=`sed -n 's/.*LIBWOLFSSL_VERSION_STRING[[:space:]]*"\([^"]*\)".*/\1/p' \ - "$(WOLFSSL_DIR)/wolfssl/version.h" | head -1`; \ - fi; \ - if [ -n "$$$$cv" ]; then \ - dep_ver="$$$$dep_ver --dep-version wolfcrypt=$$$$cv"; \ - fi; \ - ;; \ - esac; \ - fi; \ - CRA_PYTHON="$(CRA_PYTHON)" HOSTCC="$(or $($(2)HOSTCC),$(HOSTCC))" \ - "$(or $($(2)DRIVER),$(SBOM_DRIVER))" \ - --name "$($(2)NAME)" \ - --root "$(or $($(2)ROOT),$(SBOM_ROOT))" \ - --license-file "$(or $($(2)LICENSE_FILE),$(SBOM_LICENSE_FILE))" \ - --skip-missing \ - $(if $(filter 1,$($(2)NO_ARTIFACT_HASH)),--no-artifact-hash) \ - $(if $(filter 1,$($(2)NO_ARTIFACT_HASH)),, \ - $(if $(strip $($(2)SRCS)),--srcs-file "$(CURDIR)/.$(1)-wolfssl-compliance-tooling-srcs.txt")) \ - $(if $(filter 1,$($(2)NO_ARTIFACT_HASH)),, \ - $(if $($(2)LIB),--lib "$($(2)LIB)")) \ - $(if $(filter 1,$($(2)SOURCE_ONLY)),--source-only) \ - $(if $($(2)CFLAGS),--cflags="$$($(2)CFLAGS)") \ - $(if $($(2)SETTINGS_H),--settings-h "$($(2)SETTINGS_H)") \ - $(foreach d,$($(2)INCLUDE_DIRS),--include-dir "$(d)") \ - $(if $($(2)OPTIONS_H),--options-h "$($(2)OPTIONS_H)") \ - $(if $($(2)USER_SETTINGS),--user-settings "$($(2)USER_SETTINGS)") \ - $(foreach d,$($(2)USER_SETTINGS_INCLUDES),--user-settings-include "$(d)") \ - $(foreach d,$($(2)USER_SETTINGS_DEFINES),--user-settings-define "$(d)") \ - $(if $($(2)COMPONENT_TYPE),--component-type "$($(2)COMPONENT_TYPE)") \ - $(if $($(2)COPYRIGHT_START),--copyright-start "$($(2)COPYRIGHT_START)") \ - $(if $($(2)LICENSE_OVERRIDE),--license-override '$($(2)LICENSE_OVERRIDE)') \ - $(if $($(2)LICENSE_TEXT),--license-text "$($(2)LICENSE_TEXT)") \ - $(if $($(2)VERSION),--version "$($(2)VERSION)") \ - $(if $($(2)VERSION_FILE),--version-file "$($(2)VERSION_FILE)") \ - $(if $($(2)VERSION_MACRO),--version-macro "$($(2)VERSION_MACRO)") \ - $(if $($(2)DEP_WOLFSSL),--dep-wolfssl "$($(2)DEP_WOLFSSL)") \ - $(if $($(2)DEP_WOLFCRYPT),--dep-wolfcrypt "$($(2)DEP_WOLFCRYPT)") \ - $(if $($(2)DEP_OPENSSL),--dep-openssl "$($(2)DEP_OPENSSL)") \ - $(if $($(2)DEP_LIBZ),--dep-libz "$($(2)DEP_LIBZ)") \ - $(if $($(2)DOCUMENT_NAMESPACE),--document-namespace "$($(2)DOCUMENT_NAMESPACE)") \ - $(if $($(2)CRYPTO_ONLY),--crypto-only "$($(2)CRYPTO_ONLY)") \ - $$$$dep_ver \ - $(if $(or $($(2)GEN),$(GEN_SBOM)),--gen-sbom "$(or $($(2)GEN),$(GEN_SBOM))") \ - $(if $($(2)CDX_OUT),--cdx-out "$($(2)CDX_OUT)") \ - $(if $($(2)SPDX_OUT),--spdx-out "$($(2)SPDX_OUT)") +$(SBOM_TAB)@test -n "$($(2)NAME)" || { echo "ERROR: set $(2)NAME"; exit 1; } +$(SBOM_TAB)@if [ "$($(2)NO_ARTIFACT_HASH)" = "1" ]; then \ + test -z "$(strip $($(2)SRCS))$($(2)LIB)" || \ + { echo "ERROR: $(2)NO_ARTIFACT_HASH stands alone. Do not set $(2)SRCS or $(2)LIB."; exit 1; }; \ + else \ + test -n "$(strip $($(2)SRCS))$($(2)LIB)" || \ + { echo "ERROR: set $(2)SRCS or $(2)LIB"; exit 1; }; \ + fi +$(SBOM_TAB)@set -e; \ + if [ -n "$(strip $($(2)SRCS))" ]; then \ + trap 'rm -f "$(CURDIR)/.$(1)-wolfssl-compliance-tooling-srcs.txt"' EXIT INT TERM HUP; \ + printf '%s\n' $($(2)SRCS) > "$(CURDIR)/.$(1)-wolfssl-compliance-tooling-srcs.txt"; \ + fi; \ + dep_ver=""; \ + if [ -n "$($(2)DEP_VERSION)" ]; then \ + for dv in $($(2)DEP_VERSION); do \ + dep_ver="$$$$dep_ver --dep-version $$$$dv"; \ + done; \ + fi; \ + if [ "$($(2)DEP_WOLFSSL)" = "yes" ] || [ "$($(2)DEP_WOLFSSL)" = "1" ]; then \ + wv="$($(2)WOLFSSL_VERSION)"; \ + if [ -z "$$$$wv" ] && [ -n "$(WOLFSSL_DIR)" ] && \ + [ -f "$(WOLFSSL_DIR)/wolfssl/version.h" ]; then \ + wv=`sed -n 's/.*LIBWOLFSSL_VERSION_STRING[[:space:]]*"\([^"]*\)".*/\1/p' \ + "$(WOLFSSL_DIR)/wolfssl/version.h" | head -1`; \ + fi; \ + if [ -n "$$$$wv" ]; then \ + dep_ver="$$$$dep_ver --dep-version wolfssl=$$$$wv"; \ + fi; \ + fi; \ + if [ "$($(2)DEP_WOLFCRYPT)" = "yes" ] || [ "$($(2)DEP_WOLFCRYPT)" = "1" ]; then \ + case " $$$$dep_ver " in \ + *" --dep-version wolfcrypt="*) ;; \ + *) \ + cv="$($(2)WOLFSSL_VERSION)"; \ + if [ -z "$$$$cv" ] && [ -n "$(WOLFSSL_DIR)" ] && \ + [ -f "$(WOLFSSL_DIR)/wolfssl/version.h" ]; then \ + cv=`sed -n 's/.*LIBWOLFSSL_VERSION_STRING[[:space:]]*"\([^"]*\)".*/\1/p' \ + "$(WOLFSSL_DIR)/wolfssl/version.h" | head -1`; \ + fi; \ + if [ -n "$$$$cv" ]; then \ + dep_ver="$$$$dep_ver --dep-version wolfcrypt=$$$$cv"; \ + fi; \ + ;; \ + esac; \ + fi; \ + CRA_PYTHON="$(CRA_PYTHON)" HOSTCC="$(or $($(2)HOSTCC),$(HOSTCC))" \ + "$(or $($(2)DRIVER),$(SBOM_DRIVER))" \ + --name "$($(2)NAME)" \ + --root "$(or $($(2)ROOT),$(SBOM_ROOT))" \ + --license-file "$(or $($(2)LICENSE_FILE),$(SBOM_LICENSE_FILE))" \ + --skip-missing \ + $(if $(filter 1,$($(2)NO_ARTIFACT_HASH)),--no-artifact-hash) \ + $(if $(filter 1,$($(2)NO_ARTIFACT_HASH)),, \ + $(if $(strip $($(2)SRCS)),--srcs-file "$(CURDIR)/.$(1)-wolfssl-compliance-tooling-srcs.txt")) \ + $(if $(filter 1,$($(2)NO_ARTIFACT_HASH)),, \ + $(if $($(2)LIB),--lib "$($(2)LIB)")) \ + $(if $(filter 1,$($(2)SOURCE_ONLY)),--source-only) \ + $(if $($(2)CFLAGS),--cflags="$$($(2)CFLAGS)") \ + $(if $($(2)SETTINGS_H),--settings-h "$($(2)SETTINGS_H)") \ + $(foreach d,$($(2)INCLUDE_DIRS),--include-dir "$(d)") \ + $(if $($(2)OPTIONS_H),--options-h "$($(2)OPTIONS_H)") \ + $(if $($(2)USER_SETTINGS),--user-settings "$($(2)USER_SETTINGS)") \ + $(foreach d,$($(2)USER_SETTINGS_INCLUDES),--user-settings-include "$(d)") \ + $(foreach d,$($(2)USER_SETTINGS_DEFINES),--user-settings-define "$(d)") \ + $(if $($(2)COMPONENT_TYPE),--component-type "$($(2)COMPONENT_TYPE)") \ + $(if $($(2)COPYRIGHT_START),--copyright-start "$($(2)COPYRIGHT_START)") \ + $(if $($(2)LICENSE_OVERRIDE),--license-override '$($(2)LICENSE_OVERRIDE)') \ + $(if $($(2)LICENSE_TEXT),--license-text "$($(2)LICENSE_TEXT)") \ + $(if $($(2)VERSION),--version "$($(2)VERSION)") \ + $(if $($(2)VERSION_FILE),--version-file "$($(2)VERSION_FILE)") \ + $(if $($(2)VERSION_MACRO),--version-macro "$($(2)VERSION_MACRO)") \ + $(if $($(2)DEP_WOLFSSL),--dep-wolfssl "$($(2)DEP_WOLFSSL)") \ + $(if $($(2)DEP_WOLFCRYPT),--dep-wolfcrypt "$($(2)DEP_WOLFCRYPT)") \ + $(if $($(2)DEP_OPENSSL),--dep-openssl "$($(2)DEP_OPENSSL)") \ + $(if $($(2)DEP_LIBZ),--dep-libz "$($(2)DEP_LIBZ)") \ + $(if $($(2)DOCUMENT_NAMESPACE),--document-namespace "$($(2)DOCUMENT_NAMESPACE)") \ + $(if $($(2)CRYPTO_ONLY),--crypto-only "$($(2)CRYPTO_ONLY)") \ + $$$$dep_ver \ + $(if $(or $($(2)GEN),$(GEN_SBOM)),--gen-sbom "$(or $($(2)GEN),$(GEN_SBOM))") \ + $(if $($(2)CDX_OUT),--cdx-out "$($(2)CDX_OUT)") \ + $(if $($(2)SPDX_OUT),--spdx-out "$($(2)SPDX_OUT)") endef SBOM_TARGET ?= sbom From 9a2dd6e6c7f6864037e6fca21993005c461facd5 Mon Sep 17 00:00:00 2001 From: Sameeh Jubran Date: Mon, 2 Nov 2026 09:02:07 +0200 Subject: [PATCH 2/2] test(sbom.mk): check for tabs and run the recipes against a stub driver Nothing in this repo kept sbom.mk free of tabs, and no test ran it. A new recipe line with a tab passed CI here and failed only in the wolfSSL source-text check after a re-sync. tests/test_sbom_mk.py fails on any tab in sbom.mk. When make is on PATH, it runs the default target and a second $(call) target against a stub driver. It checks the driver arguments, the shell arithmetic in SBOM_CFLAGS, and the three error messages. SBOM_TAB is now an override, and sbom.mk stops with an error when $(shell printf '\t') is empty. Before, a shell without printf or SBOM_TAB=x on the command line gave "missing separator" for every make that includes the file. The comment now says why the file has no tabs. --- .github/workflows/selftest.yml | 6 +- share/build/sbom.mk | 10 ++- tests/test_sbom_mk.py | 134 +++++++++++++++++++++++++++++++++ 3 files changed, 146 insertions(+), 4 deletions(-) create mode 100644 tests/test_sbom_mk.py diff --git a/.github/workflows/selftest.yml b/.github/workflows/selftest.yml index e646523..0e0e3ad 100644 --- a/.github/workflows/selftest.yml +++ b/.github/workflows/selftest.yml @@ -64,7 +64,8 @@ jobs: tests/test_compdb_sbom.py \ tests/test_sbom_cmake_install.py \ tests/test_wolfssl_compliance_tooling_sync.py \ - tests/test_sbom_autotools.py + tests/test_sbom_autotools.py \ + tests/test_sbom_mk.py - name: Run generator unit tests run: python -m unittest tests/test_gen_sbom.py @@ -99,5 +100,8 @@ jobs: - name: Run autotools sbom.am test run: python -m unittest tests/test_sbom_autotools.py + - name: Run plain-Make sbom.mk test + run: python -m unittest tests/test_sbom_mk.py + - name: Run self-test run: python tests/test_sbom.py diff --git a/share/build/sbom.mk b/share/build/sbom.mk index 3a6d3e2..cba9995 100644 --- a/share/build/sbom.mk +++ b/share/build/sbom.mk @@ -133,9 +133,13 @@ SBOM_ROOT ?= $(CURDIR) SBOM_LICENSE_FILE ?= $(SBOM_ROOT)/LICENSE HOSTCC ?= cc -# Recipe lines in the rule below start with $(SBOM_TAB), not a literal tab. -# $(call) expands it before $(eval) parses the rule, so make still sees a tab. -SBOM_TAB := $(shell printf '\t') +# wolfSSL's source check rejects literal tabs in .mk files. Recipe lines in +# the rule below start with $(SBOM_TAB) instead. $(call) expands it before +# $(eval) parses the rule, so make still sees a tab. +override SBOM_TAB := $(shell printf '\t') +ifeq ($(SBOM_TAB),) +$(error sbom.mk needs a POSIX shell with printf to build its recipes) +endif define wolfssl_compliance_tooling_sbom_rule .PHONY: $(1) diff --git a/tests/test_sbom_mk.py b/tests/test_sbom_mk.py new file mode 100644 index 0000000..c168370 --- /dev/null +++ b/tests/test_sbom_mk.py @@ -0,0 +1,134 @@ +#!/usr/bin/env python3 +"""share/build/sbom.mk: no literal tabs, and the recipes it builds still run.""" + +import os +import shutil +import subprocess +import tempfile +import unittest +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +SBOM_MK = ROOT / "share" / "build" / "sbom.mk" +MAKE = shutil.which("make") + +STUB_DRIVER = """#!/bin/sh +for a in "$@"; do printf '%s\\n' "$a"; done > "$STUB_OUT" +""" + + +class NoTabTests(unittest.TestCase): + def test_sbom_mk_has_no_literal_tab(self): + # wolfSSL's source check rejects literal tabs in .mk files. + for number, line in enumerate( + SBOM_MK.read_text().splitlines(), start=1): + self.assertNotIn("\t", line, f"sbom.mk:{number} has a tab") + + +class RecipeTests(unittest.TestCase): + def setUp(self): + if not MAKE: + self.skipTest("make is not installed") + self.tmp = tempfile.TemporaryDirectory() + self.addCleanup(self.tmp.cleanup) + self.dir = Path(self.tmp.name) + driver = self.dir / "stub-driver" + driver.write_text(STUB_DRIVER) + driver.chmod(0o755) + self.out = self.dir / "args.txt" + + def _make(self, body, *args, after=""): + (self.dir / "Makefile").write_text( + f"SBOM_DRIVER := {self.dir / 'stub-driver'}\n" + f"{body}\n" + f"include {SBOM_MK}\n" + f"{after}\n") + env = os.environ.copy() + env["STUB_OUT"] = str(self.out) + return subprocess.run( + [MAKE, "--no-print-directory", *args], cwd=self.dir, env=env, + text=True, capture_output=True, check=False) + + def _args(self): + return self.out.read_text().splitlines() + + def test_default_target_forwards_the_driver_arguments(self): + result = self._make( + "SBOM_NAME := demo\n" + "SBOM_SRCS := a.c b.c\n" + "SBOM_CFLAGS := -DFOO=1 -DSIZE=$$(( 4 - 1 ))\n" + "SBOM_VERSION := 1.2.3\n" + "SBOM_DEP_VERSION := one=1 two=2\n") + self.assertEqual(result.returncode, 0, result.stderr) + args = self._args() + self.assertEqual(args[:2], ["--name", "demo"]) + self.assertIn("--srcs-file", args) + # The shell does the arithmetic. Make must not read $(( as a + # variable reference. + self.assertIn("--cflags=-DFOO=1 -DSIZE=3", args) + self.assertEqual(args[args.index("--version") + 1], "1.2.3") + joined = " ".join(args) + self.assertIn("--dep-version one=1 --dep-version two=2", joined) + self.assertNotIn("--lib", args) + + def test_second_target_from_call(self): + result = self._make( + "SBOM_NAME := demo\n" + "SBOM_LIB := libdemo.a\n" + "SBOM_HAL_NAME := demo-hal\n" + "SBOM_HAL_LIB := libhal.a\n", + "sbom-hal", + after="$(eval $(call wolfssl_compliance_tooling_sbom_rule," + "sbom-hal,SBOM_HAL_))") + self.assertEqual(result.returncode, 0, result.stderr) + args = self._args() + self.assertEqual(args[:2], ["--name", "demo-hal"]) + self.assertEqual(args[args.index("--lib") + 1], "libhal.a") + + def test_command_line_sbom_tab_does_not_break_the_rule(self): + result = self._make( + "SBOM_NAME := demo\nSBOM_LIB := libdemo.a\n", "SBOM_TAB=x") + self.assertEqual(result.returncode, 0, result.stderr) + self.assertIn("--lib", self._args()) + + def test_shell_without_printf_stops_with_a_clear_error(self): + false = shutil.which("false") + if not false: + self.skipTest("false is not installed") + result = self._make( + "SBOM_NAME := demo\nSBOM_LIB := libdemo.a\n", f"SHELL={false}") + self.assertNotEqual(result.returncode, 0) + self.assertIn("needs a POSIX shell with printf", result.stderr) + + def test_missing_name(self): + result = self._make("SBOM_LIB := libdemo.a\n") + self.assertNotEqual(result.returncode, 0) + self.assertIn("ERROR: set SBOM_NAME", result.stdout) + self.assertFalse(self.out.exists()) + + def test_missing_composition(self): + result = self._make("SBOM_NAME := demo\n") + self.assertNotEqual(result.returncode, 0) + self.assertIn("ERROR: set SBOM_SRCS or SBOM_LIB", result.stdout) + self.assertFalse(self.out.exists()) + + def test_no_artifact_hash_stands_alone(self): + result = self._make( + "SBOM_NAME := demo\nSBOM_LIB := libdemo.a\n" + "SBOM_NO_ARTIFACT_HASH := 1\n") + self.assertNotEqual(result.returncode, 0) + self.assertIn("SBOM_NO_ARTIFACT_HASH stands alone", result.stdout) + self.assertFalse(self.out.exists()) + + def test_no_artifact_hash_alone(self): + result = self._make( + "SBOM_NAME := demo\nSBOM_NO_ARTIFACT_HASH := 1\n") + self.assertEqual(result.returncode, 0, result.stderr) + args = self._args() + self.assertIn("--no-artifact-hash", args) + self.assertNotIn("--lib", args) + self.assertNotIn("--srcs-file", args) + + +if __name__ == "__main__": + unittest.main()