diff --git a/.github/badges/code_issues.svg b/.github/badges/code_issues.svg index 6071791..da10096 100644 --- a/.github/badges/code_issues.svg +++ b/.github/badges/code_issues.svg @@ -1 +1 @@ -code issues: 1code issues1 +code issues: 2code issues2 diff --git a/.github/badges/tests.svg b/.github/badges/tests.svg index b740c54..7afe4fa 100644 --- a/.github/badges/tests.svg +++ b/.github/badges/tests.svg @@ -1 +1 @@ -tests: 34 passedtests34 passed +tests: 36 passedtests36 passed diff --git a/code/+matbox/+setup/+internal/installFexPackage.m b/code/+matbox/+setup/+internal/installFexPackage.m index cf16696..eef2f47 100644 --- a/code/+matbox/+setup/+internal/installFexPackage.m +++ b/code/+matbox/+setup/+internal/installFexPackage.m @@ -24,9 +24,14 @@ options.AddToPath (1,1) logical = true options.AddToPathWithSubfolders (1,1) logical = true options.Verbose (1,1) logical = true - options.AgreeToLicense (1,1) logical = false + % No default, so an explicit false can be told from an omitted value + options.AgreeToLicense (1,1) logical end + isLicenseOptionGiven = isfield(options, "AgreeToLicense"); + isLicenseAgreed = isLicenseOptionGiven && options.AgreeToLicense; + isLicenseDeclined = isLicenseOptionGiven && ~options.AgreeToLicense; + % Check if toolbox is installed [isInstalled, version, toolboxFolder] = matbox.setup.internal.fex.isToolboxInstalled(toolboxIdentifier, options.Version); @@ -101,11 +106,25 @@ elseif endsWith(addonUrl, '/mltbx') [tempFilepath, C] = matbox.setup.internal.utility.tempsave(addonUrl, [toolboxIdentifier, '_temp.mltbx']); - installedAddon = matlab.addons.install(tempFilepath, options.AgreeToLicense); + isLicenseOptionSupported = isMATLABReleaseOlderThan("R2026b"); + if isLicenseOptionSupported + installedAddon = matlab.addons.install(tempFilepath, isLicenseAgreed); + else + % From R2026b, installing accepts the license agreement, + % and an agreeToLicense argument of false is an error. + installedAddon = matlab.addons.install(tempFilepath); + end if isempty(installedAddon) fprintf(newline) error('Failed to install "%s"...', toolboxName) end + if isLicenseDeclined && ~isLicenseOptionSupported + fprintf(newline) + warning("MatBox:Setup:AgreeToLicenseIgnored", ... + ['AgreeToLicense is false, but from R2026b MATLAB accepts the ', ... + 'license agreement when it installs an add-on. "%s" was installed ', ... + 'and its license terms apply.'], toolboxName) + end packageTargetFolder = string(missing); installationMethod = "mltbx"; end diff --git a/code/+matbox/+setup/installFromSourceUri.m b/code/+matbox/+setup/installFromSourceUri.m index 5e85760..807cdb5 100644 --- a/code/+matbox/+setup/installFromSourceUri.m +++ b/code/+matbox/+setup/installFromSourceUri.m @@ -23,10 +23,17 @@ options.AddToPath (1,1) logical = true options.Update (1,1) logical = false options.Verbose (1,1) logical = true - options.AgreeToLicense (1,1) logical = false + options.AgreeToLicense (1,1) logical end if startsWith(sourceUri, "fex://") + % AgreeToLicense has no default, so the installer can tell an + % explicit choice from an omitted one. Forward it only when given. + licenseArgs = {}; + if isfield(options, "AgreeToLicense") + licenseArgs = {"AgreeToLicense", options.AgreeToLicense}; + end + [packageUuid, title, version] = getFEXPackageSpecification(sourceUri); [packageTargetFolder, installationType] = matbox.setup.internal.installFexPackage( ... packageUuid, ... @@ -35,7 +42,7 @@ "Version", version, ... "AddToPath", options.AddToPath, ... "Verbose", options.Verbose, ... - "AgreeToLicense", options.AgreeToLicense); + licenseArgs{:}); installResult = struct( ... "FilePath", packageTargetFolder, ... diff --git a/code/+matbox/installRequirements.m b/code/+matbox/installRequirements.m index a225ef9..81fc0f2 100644 --- a/code/+matbox/installRequirements.m +++ b/code/+matbox/installRequirements.m @@ -12,13 +12,20 @@ function installRequirements(toolboxFolder, mode, options) options.SaveSearchPath (1,1) logical = true options.InstallationLocation (1,1) string = matbox.setup.internal.getDefaultAddonFolder() options.Verbose (1,1) logical = true - options.AgreeToLicenses (1,1) logical = false + options.AgreeToLicenses (1,1) logical end % Parse mode/flags mode = string(mode); doUpdate = any(strcmp(mode, 'update')) || any(strcmp(mode, 'u')); + % AgreeToLicenses has no default, so the installer can tell an explicit + % choice from an omitted one. Forward it only when given. + licenseArgs = {}; + if isfield(options, "AgreeToLicenses") + licenseArgs = {"AgreeToLicense", options.AgreeToLicenses}; + end + installationLocation = options.InstallationLocation; if ~isfolder(installationLocation); mkdir(installationLocation); end @@ -32,7 +39,7 @@ function installRequirements(toolboxFolder, mode, options) "AddToPath", options.UpdateSearchPath, ... "Update", doUpdate, ... "Verbose", options.Verbose, ... - "AgreeToLicense", options.AgreeToLicenses); + licenseArgs{:}); case 'Unknown' continue end diff --git a/tools/tests/+matboxtools/+unittest/SetupTest.m b/tools/tests/+matboxtools/+unittest/SetupTest.m index 36a5e97..aca8cbe 100644 --- a/tools/tests/+matboxtools/+unittest/SetupTest.m +++ b/tools/tests/+matboxtools/+unittest/SetupTest.m @@ -1,6 +1,11 @@ classdef SetupTest < matlab.unittest.TestCase % BasicTest - Unit test for testing the openMINDS tutorials. + properties (Constant) + % A FileExchange package that is distributed as an .mltbx file + FexMltbxUri = "fex://160058-recursively-list-files-and-folders" + end + methods (TestClassSetup) function setupClass(testCase) %#ok<*MANU> % pass @@ -35,9 +40,44 @@ function testUpdateGitRequirementWithGit(testCase) testCase.installRequirements("Update", true) rmpath(genpath(fullfile(pwd, "StructEditor"))) end + + function testExplicitAgreeToLicenseFalseWarns(testCase) + testCase.assumeFalse(isMATLABReleaseOlderThan("R2026b"), ... + "MATLAB ignores a declined add-on license only from R2026b.") + testCase.uninstallFexAddon(testCase.FexMltbxUri) + + testCase.verifyWarning( ... + @() matbox.setup.installFromSourceUri(testCase.FexMltbxUri, ... + "InstallationLocation", pwd, "AgreeToLicense", false), ... + "MatBox:Setup:AgreeToLicenseIgnored") + end + + function testOmittedAgreeToLicenseDoesNotWarn(testCase) + testCase.assumeFalse(isMATLABReleaseOlderThan("R2026b"), ... + "MATLAB ignores a declined add-on license only from R2026b.") + testCase.uninstallFexAddon(testCase.FexMltbxUri) + + % No warning follows the license warning in the install path, + % so the last warning shows whether it was issued. + lastwarn("", "") + matbox.setup.installFromSourceUri(testCase.FexMltbxUri, "InstallationLocation", pwd) + [~, lastWarningId] = lastwarn(); + testCase.verifyNotEqual(lastWarningId, 'MatBox:Setup:AgreeToLicenseIgnored') + end end methods + function uninstallFexAddon(~, sourceUri) + % installFexPackage skips an add-on that is already installed, so + % remove every installed version to make the next call install it. + addonIdentifier = matbox.setup.internal.fex.parseFileExchangeURI(sourceUri); + addons = matlab.addons.installedAddons(); + addonVersions = addons.Version(addons.Identifier == addonIdentifier); + for addonVersion = reshape(addonVersions, 1, []) + matlab.addons.uninstall(addonIdentifier, addonVersion) + end + end + function installRequirements(testCase, options) arguments