Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/badges/tests.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
11 changes: 11 additions & 0 deletions code/+matbox/+tasks/packageToolbox.m
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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<NASGU>

try
previousVersion = matbox.utility.getVersionFromContents(sourceFolderPath);
catch
Expand Down
61 changes: 61 additions & 0 deletions code/+matbox/+toolbox/+internal/stageRootFilesForPackaging.m
Original file line number Diff line number Diff line change
@@ -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<AGROW>
end
end

cleanupObj = onCleanup(@() deleteStagedFiles(stagedFiles));
end

function deleteStagedFiles(filePaths)
for filePath = filePaths
if isfile(filePath)
delete(filePath)
end
end
end
57 changes: 56 additions & 1 deletion tools/tests/+matboxtools/+unittest/TasksTest.m
Original file line number Diff line number Diff line change
Expand Up @@ -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
Loading