function sharedCodeUpdate(lSourceFolder, lDestSpec, varargin)
%SHAREDCODEUPDATE interactively add files to the existing shared code area
%
% SHAREDCODEUPDATE(SOURCEFOLDER, EXISTINGCODEFOLDER) copies shared code source
% files from SOURCEFOLDER to a subfolder in EXISTINGCODEFOLDER. Only source
% files that do not exist in EXISTINGCODEFOLDER are copied. This command runs
% interactively and confirmation is required before any file is copied.
%
% The copied files are placed in a subfolder under EXISTINGCODEFOLDER
% corresponding to the release in which they were originally generated, if
% this information can be determined. The subfolder is created, if it does not
% already exist.
%
% SHAREDCODEUPDATE(BUILDFOLDER, EXISTINGCODEFOLDER) copies shared code source
% files from the shared code location associated with the build folder in
% in BUILDFOLDER.
%
% SHAREDCODEUPDATE(PROTECTEDMODEL, EXISTINGCODEFOLDER) extracts shared
% source code files from the shared code location contained in the
% protected model PROTECTEDMODEL.
%
% SHAREDCODEUPDATE(BUILDFOLDER, CONFIGSETORMODEL) sets
% EXISTINGCODEFOLDER to the value of the 'ExistingSharedCode' parameter
% from the Simulink configuration set or model CONFIGSETORMODEL.
%
% To explicitly specify the name of the subfolder under EXISTINGCODEFOLDER, use
% SHAREDCODEUPDATE(SOURCEFOLDER, EXISTINGCODEFOLDER, 'ExistingCodeSubfolder',
% <destination subfolder>)
%

% Copyright 2016-2023 The MathWorks, Inc.

% Convert any string input to char
lSourceFolder = convertStringsToChars(lSourceFolder);
lDestSpec = convertStringsToChars(lDestSpec);
[varargin{:}] = convertStringsToChars(varargin{:});

persistent c1; %#ok<PUSE>
c1 = []; % Used for an "onCleanup" object for the temporary folder used for unpacking SLXPs

p = inputParser;
addRequired(p, 'SourceFolder', @ischar);
addRequired(p, 'DestinationFolder', @(x)ischar(x) || isa(x,'Simulink.ConfigSet') || ishandle(x));
addParameter(p, 'ExistingCodeSubfolder', subfolderUnassigned, @ischar);
addParameter(p, 'Interactive', true, @(x)i_isConvertibleToLogical(x));
addParameter(p, 'CodeLocation', '', @ischar);
addParameter(p, 'CopyForLibCodeGen',false, @(x) i_isConvertibleToLogical(x))
addParameter(p, 'SourceExt','.c', @ischar) % only used for library-based code generation
parse(p, lSourceFolder, lDestSpec, varargin{:});

lDestinationSubFolder = p.Results.ExistingCodeSubfolder;
isInteractive = logical(p.Results.Interactive);
codeLocationParam = p.Results.CodeLocation;
copyForLibCodeGen = p.Results.CopyForLibCodeGen;
sourceExt = p.Results.SourceExt;

% For backwards compatibility in case a user points sharedCodeUpdate to
% an old artifact created by crossReleaseExport in R2018b or earlier
isArtifactLocation = i_sourceFolderIsArtifactLocation(lSourceFolder);
if isArtifactLocation
% Determine whether the source folder is actually a build folder (in
% which case we need to determine the real location of the shared code)
lIsSharedCodeUpdate = true;
lBuildFolderLocationValidator = coder.internal.xrel.BuildFolderLocationValidator(lIsSharedCodeUpdate);
lSourceFolder = lBuildFolderLocationValidator.validate(lSourceFolder, codeLocationParam);
isSLXP = false;
isBuildFolder = true;
elseif slInternal('hasProtectedSimulinkExtension', lSourceFolder)
isSLXP = true;
isBuildFolder = false;
else
isSLXP = false;
isBuildFolder = i_sourceFolderIsBuildFolder(lSourceFolder);
end

if isBuildFolder
lInfoStructLoader = coder.internal.xrel.buildartifacts.InfoStructLoader;
lInfoStructLoader.load(lSourceFolder);
lInfoStruct = lInfoStructLoader.InfoStruct;
lRelativePathToAnchor = lInfoStruct.relativePathToAnchor;
lSharedSourcesDir = lInfoStruct.sharedSourcesDir;
lSourceCodeFolder = coder.internal.xrelexport.getCanonicalPath(fullfile(lSourceFolder, lRelativePathToAnchor, lSharedSourcesDir), pwd);
lSourceToDisplay = lSourceCodeFolder;
elseif isSLXP
% The user cannot know up front whether the SLXP contains shared code
% or not. If there's nothing, then we don't want to throw an error or
% a warning, but we should instead present an informational message.
[~, slxpFile, slxpExt] = fileparts(lSourceFolder);
slxpFileWithExt = [slxpFile, slxpExt];
if coder.internal.xrel.protectedModelHasSharedUtilityCode(lSourceFolder)
scratchLocation = tempname;
mkdir(scratchLocation);
c1 = onCleanup(@()rmdir(scratchLocation, 's'));
relativeDstLoc = coder.internal.xrel.unpackSharedCodeFromProtectedModel(lSourceFolder, scratchLocation);
lSourceCodeFolder = fullfile(scratchLocation, relativeDstLoc);
lSourceToDisplay = sprintf('%s:%s', slxpFileWithExt, relativeDstLoc);
else
fprintf(1, '### %s\n', ...
DAStudio.message('coder_xrel:crossRelease:ProtectedModelNoSharedUtils', slxpFileWithExt));
return;
end
else
% Otherwise the source folder is simply the source code folder
lSourceCodeFolder = lSourceFolder;
lSourceToDisplay = lSourceCodeFolder;
end

% Determine if the second argument is a configset or model
[aDonorCS, isModel, isModelHandle] = coder.internal.xrel.ImportConfigSetManager.getImportConfigSet(lDestSpec);
if ~isempty(aDonorCS)
lExistingSharedCode = get_param(aDonorCS, 'ExistingSharedCode');
if isempty(lExistingSharedCode)
if isModel
if isModelHandle
donorCSName = get(lDestSpec, 'Name');
else
donorCSName = lDestSpec; % Name of model
end
else
donorCSName = inputname(2); % Name of configset - only used for user feedback in error message
end
error(message('coder_xrel:crossRelease:ExistingSharedCodeNotSet', donorCSName));
end
else
lExistingSharedCode = lDestSpec;
end
i_sharedCodeUpdate(lSourceCodeFolder, ...
lSourceToDisplay, ...
lExistingSharedCode, ...
lDestinationSubFolder, ...
isInteractive, ...
copyForLibCodeGen,...
sourceExt);

function str = subfolderUnassigned()
str = '__SUBFOLDER_UNASSIGNED__';

function i_sharedCodeUpdate(lSourceFolder, lSourceToDisplay, lExistingSharedCode, lDestinationSubFolder, isInteractive, copyForLibCodeGen, sourceExt)

% Check that the specified source folder exists and contains source code
lSourceFolderFiles = {};
if exist(lSourceFolder, 'dir')
% Identify potential files to copy located in the source folder
if copyForLibCodeGen
% Only copy of shared function files. Other shared utilities can
% remain in slprj
lSourceFolderFiles = coder.internal.xrel.getSharedFunctionSources...
(lSourceFolder, sourceExt);
else
lSourceFolderFiles = coder.internal.xrel.getSharedUtilitySources...
(lSourceFolder);
end
end

% When Language Standard Types are enabled, a model that is configured to
% nominally use shared utilities may end up with a shared utilities folder
% that contains no source or header files (for example in the case where
% with coder types, the only shared utility file would have been
% rtwtypes.h). In this instance, there will at least be a file
% checksummap.mat or rtwtypeschksum.mat present - and we shouldn't error
% out in this instance - simply we should proceed and copy no files.
foundChecksummap = isfile(fullfile(lSourceFolder, 'checksummap.mat'));
foundRtwtypeschksum = isfile(fullfile(lSourceFolder, 'rtwtypeschksum.mat'));
if isempty(lSourceFolderFiles) && ...
~copyForLibCodeGen && ...
~foundChecksummap && ...
~foundRtwtypeschksum
DAStudio.error('coder_xrel:crossRelease:NoSourceFilesToCopy', lSourceFolder);
end

% Check that the specified destination folder exists
if ~exist(lExistingSharedCode, 'dir')
DAStudio.error('coder_xrel:crossRelease:NoExistingSharedCodeFolder', ...
lExistingSharedCode);
end

if strcmp(lDestinationSubFolder, subfolderUnassigned)
lDestinationSubFolder = i_getDefaultSubfolder(lSourceFolder);
end

% Identify existing shared code
[lRepositorySourceFiles, lRepositorySourceFileFolders] = ...
coder.internal.xrel.getExistingSharedCode(lExistingSharedCode);

% Text for interactive messages
continueQuery = DAStudio.message('coder_xrel:crossRelease:Continue_Query');
yes = DAStudio.message('coder_xrel:crossRelease:Continue_Yes');
no = DAStudio.message('coder_xrel:crossRelease:Continue_No');

% Notify user about files in the source folder with same name but different
% content compared to corresponding file in the existing code area
[requestContinue, filesToOverWrite]= i_notifyDiffContentFiles...
(lRepositorySourceFileFolders, lRepositorySourceFiles, ...
isInteractive, ...
lSourceFolder, ...
lSourceToDisplay, ...
lSourceFolderFiles, ...
lExistingSharedCode, ...
continueQuery, yes, no,...
copyForLibCodeGen);
if ~requestContinue
return;
end

% Identify the files that must be updated
newFiles = setdiff(lSourceFolderFiles, lRepositorySourceFiles);

if (copyForLibCodeGen)
newFiles = [newFiles filesToOverWrite];
end
if isempty(newFiles)
disp(DAStudio.message('coder_xrel:crossRelease:NoFilesCopied'));
return
end

% Copy files to the shared code area
i_doUpdateFiles(lExistingSharedCode, lDestinationSubFolder, ...
isInteractive, ...
lSourceFolder, ...
lSourceToDisplay, ...
newFiles, ...
continueQuery, yes, no, ...
copyForLibCodeGen);

function result = i_isConvertibleToLogical(val)
result = islogical(val) || isnumeric(val);

function sourceVersion = i_getSourceVersionFromSCMFile(scmFile)

sourceVersion = '';

% Make a copy of the scmFile because opening it directly may modify it and make
% it unreadable from the earlier release that originally created it
tempDir = tempname;
mkdir(tempDir)
c = onCleanup(@()rmdir(tempDir, 's'));
tempScmFile = fullfile(tempDir, 'temp_shared_file.dmr');
copyfile(scmFile, tempScmFile);

interface = SharedCodeManager.SharedTypesInterface(tempScmFile);
metamodelVersion = interface.getMetamodelVersion();
if ~isempty(metamodelVersion)
sourceVersion = metamodelVersion;
end

delete(interface);
delete(c);

function subfolder = i_getDefaultSubfolder(lSourceFolder)

sourceVersion = '';

% Check for shared code manager file
scmFile = fullfile(lSourceFolder, 'shared_file.dmr');
if exist(scmFile, 'file')
sourceVersion = i_getSourceVersionFromSCMFile(scmFile);
end

if isempty(sourceVersion)
% Look for checksummap.mat file
checksumFile = fullfile(lSourceFolder, 'checksummap.mat');
if exist(checksumFile, 'file')
sourceVersion = '2015a_or_earlier';
end
end

if ~isempty(sourceVersion)
subfolder = sprintf('R%s', sourceVersion);
else
% If it doesn't look like a build folder, derive default name from the source
% folder
[~, f] = fileparts(lSourceFolder);
subfolder = f;
end

function responseString = i_get_user_response(queryString, validResponses)

s = input(queryString, 's');
responseString = coder.internal.xrel.scu_validatestring(s, validResponses);
requiredResponse = DAStudio.message...
('coder_xrel:crossRelease:Continue_RequiredResponse', ...
validResponses{1}, validResponses{2});
while isempty(responseString)
fprintf('%s\n', requiredResponse);
s = input(queryString, 's');
responseString = coder.internal.xrel.scu_validatestring(s, validResponses);
end

function [requestContinue, filesToOverWrite] = i_notifyDiffContentFiles...
(lRepositorySourceFileFolders, lRepositorySourceFiles, ...
isInteractive, ...
lSourceFolder, ...
lSourceToDisplay, ...
lSourceFolderFiles, ...
lExistingSharedCode, ...
continueQuery, yes, no,...
copyForLibCodeGen)
requestContinue = true;
filesToOverWrite = [];
[~, ~, ib] = intersect(lSourceFolderFiles, lRepositorySourceFiles);
existingFiles = lRepositorySourceFiles(ib);
repoFoldersExistingFiles = lRepositorySourceFileFolders(ib);

% For each existing file determine if the local copy differs from what is in the
% existing shared code area
existingFilesContentDiffersIdx = true(size(existingFiles));
for i=1:length(existingFiles)
fileName = existingFiles{i};
repoFolder = repoFoldersExistingFiles{i};
repositoryContent = fileread(fullfile(repoFolder, fileName));
localContent = fileread(fullfile(lSourceFolder, fileName));
if strcmp(repositoryContent, localContent)
existingFilesContentDiffersIdx(i) = false;
end
end
existingFilesContentDiffers = existingFiles(existingFilesContentDiffersIdx);
repoFoldersContentDiffers = ...
repoFoldersExistingFiles(existingFilesContentDiffersIdx);

if ~isempty(existingFilesContentDiffers)
fileListItems = "";
if (copyForLibCodeGen)
% If this is a library code generation scenario, we want to
% overwrite source/header files from the source folder to the
% library's cached code folder
filesToOverWrite = existingFilesContentDiffers;
return;
end
for i=1:length(existingFilesContentDiffers)
fileName = existingFilesContentDiffers{i};
diffCmd = sprintf('<a href="matlab:visdiff(''%s'', ''%s'')">compare</a>', ...
fullfile(lSourceFolder, fileName), ...
fullfile(repoFoldersContentDiffers{i}, fileName));
fileListItems(i) = string(sprintf(' %s [%s]', fileName, diffCmd));
end

fileList = char(join(fileListItems, newline));
fileList = sprintf('%s\n', fileList);

disp(DAStudio.message('coder_xrel:crossRelease:ExistingFilesNotCopied', ...
lExistingSharedCode, lSourceToDisplay, fileList));
fprintf('\n')
continueQueryString = sprintf('%s %s/%s: ', continueQuery, yes, no);

if isInteractive
responseString = i_get_user_response(continueQueryString, {yes, no});
else
responseString = yes;
end

if strcmpi(responseString, no)
requestContinue = false;
end
end

function i_doUpdateFiles(lExistingSharedCode, lDestinationSubFolder, ...
isInteractive, ...
lSourceFolder, ...
lSourceToDisplay, ...
newFiles, ...
continueQuery, yes, no, ...
copyForLibCodeGen)

% Make sure destination folder exists
destFolder = fullfile(lExistingSharedCode, lDestinationSubFolder);
if ~exist(destFolder, 'dir')
mkdir(destFolder)
end

fprintf('\n%s\n', ...
DAStudio.message('coder_xrel:crossRelease:FilesWillBeCopied', ...
lSourceToDisplay, destFolder));
fileList = sprintf(' %s\n', newFiles{:});
fprintf('\n%s\n', fileList);

continueQueryString = sprintf('%s %s/%s: ', continueQuery, yes, no);

if isInteractive
responseString = i_get_user_response(continueQueryString, {yes, no});
else
responseString = yes;
end

if ~strcmpi(responseString, yes)
return
end

% DMR files should not be copied, they should be merged
newFiles = i_mergeDMR(newFiles, lSourceFolder, destFolder );

if copyForLibCodeGen
for i=1:length(newFiles)
newFileNoPath = newFiles{i};
newFile = fullfile(lSourceFolder, newFileNoPath);
if strcmp(newFiles{i},'shared_file.dmr')
% Special Processing shared_file.dmr
i_processSharedDMR(lSourceFolder, destFolder);
else
% Blindly move the rest of the src/header files to library code
% folder
movefile(newFile, fullfile(destFolder, newFiles{i}));
end
end
else
% Identify any source files for reusable library subsystem functions.
% This only includes code generated in the current release, because
% these functions cannot be shared across releases.
[sharedFunctionFileNames, allSFIdens, allSFData] = ...
i_getReusableLibrarySharedFunctions(lSourceFolder);

for i=1:length(newFiles)
newFileNoPath = newFiles{i};
newFile = fullfile(lSourceFolder, newFileNoPath);
copyfile(newFile, fullfile(destFolder, newFiles{i}));

% If the file corresponds to a reusable library subsystem function,
% additionally write out checksum data in a corresponding .json
% file to enable reuse
sharedFunctionIdx = strcmp(sharedFunctionFileNames, newFileNoPath);

if any(sharedFunctionIdx)
assert(sum(sharedFunctionIdx)==1, 'There must be exactly one match')
i_writeSharedFunctionJSON(allSFIdens{sharedFunctionIdx}, ...
allSFData{sharedFunctionIdx}, newFileNoPath, destFolder);
end
end
end

disp(DAStudio.message('coder_xrel:crossRelease:FilesCopied', ...
lSourceToDisplay, destFolder));

function isBuildFolder = i_sourceFolderIsBuildFolder(sourceFolder)
% I_SOURCEFOLDERISBUILDFOLDER
% Returns true if the source folder describes a build folder
% folder rather than a shared code repository

dirContents = dir(sourceFolder);
fileNames = {dirContents.name};

% Check for codeInfo, model reference codeInfo or codedescriptor.
hasCodeDescriptor = ...
any(cellfun(@(x)~isempty(x), ...
regexp(fileNames, '(codeInfo\.mat)|(\w*_codeInfo_mr\.mat)|(codedescriptor\.dmr)')));
hasProjFile = ismember('rtw_proj.tmw', fileNames);
isBuildFolder = hasCodeDescriptor && hasProjFile;

function isArtifactLocation = i_sourceFolderIsArtifactLocation(sourceFolder)
% I_SOURCEFOLDERISARTIFACTLOCATION
% Returns true if the source folder describes a cross release artifact
% folder rather than a shared code repository
isArtifactLocation = isfile(fullfile(sourceFolder, 'crossReleaseInfo.m'));

function newFiles = i_mergeDMR(newFiles, lSourceFolder, destFolder )

% This function is used to support incremental code generation for
% libraries. If a harness needs to be rebuilt due to changes made by
% the user, we need to merge the shared_file.dmr produced by this harness
% with the master shared_file.dmr owned by the library.

sourceDBloc = fullfile(lSourceFolder,'shared_file.dmr');
destDBloc = fullfile(destFolder,'shared_file.dmr');
if ~isfile(destDBloc)
% Nothing to merge, just copy the whole file
return;
end

% There is a shared_file.dmr to be merged. Remove that entry from
% newFiles array so that it is not copied as is, but merged correctly
dmrFileIdx = find(strcmp(newFiles,'shared_file.dmr'));
newFiles(dmrFileIdx)=[]; %#ok

% Make sure that the database metamodel versions match.
sourceDBver = i_getSourceVersionFromSCMFile(sourceDBloc);
destDBver = i_getSourceVersionFromSCMFile(destDBloc);

if ~strcmp(sourceDBver, destDBver)
error(message('Simulink:librarycodegen:IncompatibleDatabaseVersions'));
end

% Merge Shared Functions
sourceDBIf = SharedCodeManager.SharedFunctionInterface(sourceDBloc);
destDBIf = SharedCodeManager.SharedFunctionInterface(destDBloc);
sfIdens = sourceDBIf.retrieveAllIdentities('SCM_SHARED_FUNCTIONS');
for i = 1:length(sfIdens)
% Delete the already existing identity in destDB so that we can
% replace it.
funcName = sourceDBIf.retrieveData(sfIdens{i}).FunctionName;
destIden = destDBIf.retrieveIdentityForName('SCM_SHARED_FUNCTIONS',funcName);
if ~isempty(destIden)
destDBIf.removeIdentityAndData(destIden);
end

% Register this shared function in the destination database
destDBIf.registerDataUsingCaching(sfIdens{i},sourceDBIf.retrieveData(sfIdens{i}) );

% Now delete the shared function entry from the source database
sourceDBIf.removeIdentityAndData(sfIdens{i});
end

% Merge Library Subsystem Information
sourceDBIf = SharedCodeManager.LibrarySubsystemInterface(sourceDBloc);
destDBIf = SharedCodeManager.LibrarySubsystemInterface(destDBloc);
lsIdens = sourceDBIf.retrieveAllIdentities('SCM_LIBRARY_SUBSYSTEM');
for i = 1:length(lsIdens)
destDBIf.registerDataUsingCaching(lsIdens{i},sourceDBIf.retrieveData(lsIdens{i}) );
sourceDBIf.removeIdentityAndData(lsIdens{i});
end

% Merge Code Generation Contexts
cgCtxIden = sourceDBIf.retrieveAllIdentities('SCM_CODEGEN_CONTEXT');
for i = 1:length(cgCtxIden)
destDBIf.registerDataUsingCaching(cgCtxIden{i},sourceDBIf.retrieveData(cgCtxIden{i}) );
sourceDBIf.removeIdentityAndData(cgCtxIden{i});
end

% Force registration of SCM entities
destDBIf.finalize();

function i_processSharedDMR(sourceFolder, destFolder)
% We need to keep a copy of the shared_file.dmr in the library slprj folder
% because models that build into the same slprj need to know about the shared
% utilities available in the slprj folder. We will delete the shared
% function information from this source database because the shared
% function source/header files have already been moved to the library folder

% First, copy over the file from source to destination
copyfile(fullfile(sourceFolder, 'shared_file.dmr'), fullfile(destFolder, 'shared_file.dmr'));

% Now, delete all the shared functions from the shared database at source
libIntf = SharedCodeManager.LibrarySubsystemInterface(fullfile(sourceFolder, 'shared_file.dmr'));
sfIdens = libIntf.retrieveAllIdentities('SCM_SHARED_FUNCTIONS');
for i = 1:length(sfIdens)
libIntf.removeIdentityAndData(sfIdens{i});
end

cgCtxIdens = libIntf.retrieveAllIdentities('SCM_CODEGEN_CONTEXT');
for i = 1:length(cgCtxIdens)
libIntf.removeIdentityAndData(cgCtxIdens{i});
end

libSSIdens = libIntf.retrieveAllIdentities('SCM_LIBRARY_SUBSYSTEM');
for i = 1:length(libSSIdens)
libIntf.removeIdentityAndData(libSSIdens{i});
end

function [sharedFunctionFileNames, allSFIdens, allSFData] = ...
i_getReusableLibrarySharedFunctions(lSourceFolder)

sharedFunctionFileNames = {};
allSFIdens = [];
allSFData = [];

% If the source folder contains a shared_file.dmr, extract the release
% number
scmFile = fullfile(lSourceFolder,'shared_file.dmr');
if ~isfile(scmFile)
return
end
releaseFromSourceDB = i_getSourceVersionFromSCMFile(scmFile);

% Determine if shared_file.dmr in source folder was generated in the
% current release
sourceFolderFromCurrentRelease = strcmp(releaseFromSourceDB, version('-release'));

if sourceFolderFromCurrentRelease
sfIntf = SharedCodeManager.SharedFunctionInterface(scmFile);
allSFIdens = sfIntf.retrieveAllIdentities('SCM_SHARED_FUNCTIONS');
allSFData = sfIntf.retrieveAllData('SCM_SHARED_FUNCTIONS');

sharedFunctionFileNames = cell(size(allSFData));
for i=1:length(allSFData)
sharedFunctionFileNames{i} = allSFData{i}.FileName;
end
end

function i_writeSharedFunctionJSON(sfIden, sfData, newFileNoPath, destFolder)
sharedFunctionStruct = struct ...
('Release', {version('-release')}, ...
'ChecksumElement1', {sfIden.ChecksumElement1}, ...
'ChecksumElement2', {sfIden.ChecksumElement2}, ...
'ChecksumElement3', {sfIden.ChecksumElement3}, ...
'ChecksumElement4', {sfIden.ChecksumElement4}, ...
'FunctionName', {sfData.FunctionName}, ...
'FileName', {sfData.FileName}, ...
'BlockSID', {sfData.BlockSID}, ...
'ClientSIDs', {sfData.ClientSIDs}, ...
'LibraryBlockPath', {sfData.LibraryBlockPath});

[~, jsonFileName] = fileparts(newFileNoPath);
jsonFileName = fullfile(destFolder, [jsonFileName '.json']);
jsonData = jsonencode(sharedFunctionStruct);

% Write out the .json file with UTF-8 encoding
fid = fopen(jsonFileName, 'w');
content = unicode2native(jsonData, 'UTF-8');
fwrite(fid, content, 'uint8');
fclose(fid);