From 488e150b938eeccdcaa7ea2e784ce2c57398d906 Mon Sep 17 00:00:00 2001 From: ehennestad Date: Thu, 16 Jul 2026 20:31:33 +0200 Subject: [PATCH 1/2] feat: include project root files in packaged toolbox ToolboxOptions only packages files below the source folder, so files like LICENSE that conventionally live in the project root were left out of the .mltbx. Root files are now staged into the source folder before packaging and cleaned up afterwards. The files to include are declared in a top-level RootFilesToPackage list in MLToolboxInfo.json, defaulting to LICENSE. Explicitly listed files that are missing trigger a warning, as does a root file shadowed by an identically named file in the source folder. Co-Authored-By: Claude Fable 5 --- code/+matbox/+tasks/packageToolbox.m | 11 ++++ .../+internal/stageRootFilesForPackaging.m | 61 +++++++++++++++++++ .../tests/+matboxtools/+unittest/TasksTest.m | 57 ++++++++++++++++- 3 files changed, 128 insertions(+), 1 deletion(-) create mode 100644 code/+matbox/+toolbox/+internal/stageRootFilesForPackaging.m diff --git a/code/+matbox/+tasks/packageToolbox.m b/code/+matbox/+tasks/packageToolbox.m index 2b57f0f..90660ba 100644 --- a/code/+matbox/+tasks/packageToolbox.m +++ b/code/+matbox/+tasks/packageToolbox.m @@ -12,6 +12,11 @@ % packageTookbox('specific', versionString) VERSIONSTRING is a string containing % the specific 3 part semantic version (i.e. "2.3.4") to use. % +% By default, a LICENSE file in the project root directory is included in +% the packaged toolbox. To control which project root files are included, +% specify a top-level "RootFilesToPackage" list in MLToolboxInfo.json, +% e.g. "RootFilesToPackage": ["LICENSE", "THIRD_PARTY_NOTICES.md"]. +% % Adapted from: https://github.com/mathworks/climatedatastore/blob/main/buildUtilities/packageToolbox.m % Todo: @@ -32,6 +37,12 @@ % Get updated version number sourceFolderPath = fullfile(projectRootDirectory, options.SourceFolderName); + + % Temporarily copy project root files (e.g. LICENSE) into the source + % folder so they are included in the packaged toolbox. + stagedFilesCleanupObj = matbox.toolbox.internal.stageRootFilesForPackaging(... + projectRootDirectory, sourceFolderPath); %#ok + try previousVersion = matbox.utility.getVersionFromContents(sourceFolderPath); catch diff --git a/code/+matbox/+toolbox/+internal/stageRootFilesForPackaging.m b/code/+matbox/+toolbox/+internal/stageRootFilesForPackaging.m new file mode 100644 index 0000000..c58a3fd --- /dev/null +++ b/code/+matbox/+toolbox/+internal/stageRootFilesForPackaging.m @@ -0,0 +1,61 @@ +function cleanupObj = stageRootFilesForPackaging(projectRootDirectory, sourceFolderPath) +% stageRootFilesForPackaging - Temporarily copy project root files into the source folder +% +% cleanupObj = stageRootFilesForPackaging(projectRootDirectory, sourceFolderPath) +% copies selected files from the project root directory into the toolbox +% source folder so that they are included in the packaged toolbox. +% ToolboxOptions only packages files located below the source folder, so +% files like LICENSE that conventionally live in the project root must be +% staged before packaging. The returned onCleanup object deletes the +% staged copies when it goes out of scope. +% +% The files to stage are read from the top-level "RootFilesToPackage" +% field of MLToolboxInfo.json. If the field is not present, the default +% is "LICENSE"; a missing default file is skipped silently, whereas a +% missing explicitly listed file triggers a warning. + + arguments + projectRootDirectory (1,1) string {mustBeFolder} + sourceFolderPath (1,1) string {mustBeFolder} + end + + [~, ~, toolboxInfo] = matbox.toolbox.readToolboxInfo(projectRootDirectory); + + if isfield(toolboxInfo, 'RootFilesToPackage') + fileNames = reshape(string(toolboxInfo.RootFilesToPackage), 1, []); + warnIfMissing = true; + else + fileNames = "LICENSE"; + warnIfMissing = false; + end + + stagedFiles = string.empty; + for fileName = fileNames + sourceFile = fullfile(projectRootDirectory, fileName); + targetFile = fullfile(sourceFolderPath, fileName); + if ~isfile(sourceFile) + if warnIfMissing + warning("MatBox:Package:RootFileNotFound", ... + 'The file "%s" is listed in "RootFilesToPackage" in MLToolboxInfo.json, but was not found in the project root directory.', ... + fileName) + end + elseif isfile(targetFile) + warning("MatBox:Package:RootFileShadowed", ... + 'The source folder already contains a file named "%s". The existing file will be packaged instead of the project root file.', ... + fileName) + else + copyfile(sourceFile, targetFile) + stagedFiles(end+1) = targetFile; %#ok + end + end + + cleanupObj = onCleanup(@() deleteStagedFiles(stagedFiles)); +end + +function deleteStagedFiles(filePaths) + for filePath = filePaths + if isfile(filePath) + delete(filePath) + end + end +end diff --git a/tools/tests/+matboxtools/+unittest/TasksTest.m b/tools/tests/+matboxtools/+unittest/TasksTest.m index a6e02f8..40073a1 100644 --- a/tools/tests/+matboxtools/+unittest/TasksTest.m +++ b/tools/tests/+matboxtools/+unittest/TasksTest.m @@ -41,8 +41,63 @@ function testPackageToolbox(testCase) rmdir(fullfile(pwd, 'releases'), 's') mkdir(fullfile(pwd, 'releases')) end - matbox.tasks.packageToolbox(pwd, "build", "", "SourceFolderName", "code") + [~, toolboxFile] = matbox.tasks.packageToolbox( ... + pwd, "build", "", "SourceFolderName", "code"); testCase.verifyTrue(isfolder(fullfile(pwd, "releases"))) + + archiveFolder = fullfile(pwd, "toolbox-archive"); + unzip(toolboxFile, archiveFolder) + packagedLicenseFile = fullfile(archiveFolder, "fsroot", "LICENSE"); + testCase.verifyTrue(isfile(packagedLicenseFile)) + testCase.verifyEqual(fileread(packagedLicenseFile), ... + fileread(fullfile(pwd, "LICENSE"))) + + % Staged copy is removed from the source folder after packaging + testCase.verifyFalse(isfile(fullfile(pwd, "code", "LICENSE"))) + end + + function testPackageToolboxWithRootFilesToPackage(testCase) + pathStr = matboxtools.projectdir(); + copyfile(pathStr, pwd); + + % Add a notices file and declare an explicit list of root files + % to package, including one file that does not exist. + matbox.utility.filewrite(fullfile(pwd, 'NOTICE.md'), 'Third party notices'); + + toolboxInfoFile = fullfile(pwd, 'tools', 'MLToolboxInfo.json'); + toolboxInfo = jsondecode(fileread(toolboxInfoFile)); + toolboxInfo.RootFilesToPackage = {'LICENSE'; 'NOTICE.md'; 'MISSING.md'}; + matbox.utility.filewrite(toolboxInfoFile, ... + jsonencode(toolboxInfo, "PrettyPrint", true)); + + [~, toolboxFile] = testCase.verifyWarning(... + @() matbox.tasks.packageToolbox(pwd, "build", "", "SourceFolderName", "code"), ... + "MatBox:Package:RootFileNotFound"); + + archiveFolder = fullfile(pwd, "toolbox-archive"); + unzip(toolboxFile, archiveFolder) + testCase.verifyTrue(isfile(fullfile(archiveFolder, "fsroot", "LICENSE"))) + testCase.verifyTrue(isfile(fullfile(archiveFolder, "fsroot", "NOTICE.md"))) + end + + function testPackageToolboxShadowedRootFile(testCase) + pathStr = matboxtools.projectdir(); + copyfile(pathStr, pwd); + + % A file with the same name in the source folder shadows the + % project root file and must not be overwritten or deleted. + shadowText = 'Shadowing license file'; + matbox.utility.filewrite(fullfile(pwd, 'code', 'LICENSE'), shadowText); + + [~, toolboxFile] = testCase.verifyWarning(... + @() matbox.tasks.packageToolbox(pwd, "build", "", "SourceFolderName", "code"), ... + "MatBox:Package:RootFileShadowed"); + + archiveFolder = fullfile(pwd, "toolbox-archive"); + unzip(toolboxFile, archiveFolder) + packagedLicenseFile = fullfile(archiveFolder, "fsroot", "LICENSE"); + testCase.verifyEqual(fileread(packagedLicenseFile), shadowText) + testCase.verifyTrue(isfile(fullfile(pwd, 'code', 'LICENSE'))) end end end From dc8009bee99fd2553041249798bcc22ab55b4a8a Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 16 Jul 2026 18:35:07 +0000 Subject: [PATCH 2/2] Update GitHub badges --- .github/badges/tests.svg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/badges/tests.svg b/.github/badges/tests.svg index 3433011..b2979d5 100644 --- a/.github/badges/tests.svg +++ b/.github/badges/tests.svg @@ -1 +1 @@ -teststests21 passed21 passed \ No newline at end of file +teststests23 passed23 passed \ No newline at end of file