Program Listing for File compileAndLinkModel.m

Return to documentation for file (matlab/@amimodel/compileAndLinkModel.m)

function compileAndLinkModel(modelname, modelSourceFolder, coptim, debug, funs, cfun)
    % compileAndLinkModel compiles the mex simulation file.
    % It does not check if the model files have changed since generating
    % C++ code or whether all files are still present.
    % Use only if you know what you are doing. The safer alternative is
    % rerunning amiwrap().
    %
    % Parameters:
    %  modelname: name of the model as specified for amiwrap()
    %  modelSourceFolder: path to model source directory
    %  coptim: optimization flags
    %  debug: enable debugging
    %  funs: array with names of the model functions, will be guessed
    %   from source files if left empty
    %  cfun: struct indicating which files should be recompiled
    %
    % Return values:
    %  void

    % if no list provided, try to determine relevant files from model
    % folder
    if(isempty(funs))
        ls = dir(fullfile(modelSourceFolder, [modelname '_*.cpp']));
        ls = {ls.name};
        % extract funs from filename (strip of modelname_ and .cpp
        funs = cellfun(@(x) x((length(modelname)+2):(length(x)-4)), ls, 'UniformOutput', false);
    end

    objectFileSuffix = '.o';
    if(ispc)
        objectFileSuffix = '.obj';
    end

    % compile flags
    COPT = ['COPTIMFLAGS=''' coptim ' -DNDEBUG'' CXXFLAGS=''$CXXFLAGS -std=c++14'''];
    if(debug)
        DEBUG = ' -g CXXFLAGS=''$CXXFLAGS -Wall  -std=c++14 -Wno-unused-function -Wno-unused-variable'' ';
        COPT = ''; % no optimization with debug flags!
    else
        DEBUG = '';
    end

    compilerVersion = getCompilerVersionString();
    amiciRootPath = fileparts(fileparts(fileparts(mfilename('fullpath'))));
    baseObjectFolder = fullfile(amiciRootPath,'models',mexext,version('-release'),compilerVersion);
    baseModelObjectFolder = fullfile(modelSourceFolder,version('-release'),compilerVersion);
    if(debug)
        objectFolder = fullfile(baseObjectFolder, 'debug');
        modelObjectFolder = fullfile(baseModelObjectFolder, 'debug');
    else
        objectFolder = fullfile(baseObjectFolder, 'release');
        modelObjectFolder = fullfile(baseModelObjectFolder, 'release');
    end
    % compile directory
    if(~exist(objectFolder, 'dir'))
        mkdir(objectFolder);
    end
    if(~exist(modelObjectFolder, 'dir'))
        mkdir(modelObjectFolder);
    end

    %% Third party libraries
    dependencyPath = fullfile(amiciRootPath, 'ThirdParty');
    gslPath = fullfile(dependencyPath, 'gsl');
    [objectsstr, includesstr] = compileAMICIDependencies(dependencyPath, objectFolder, objectFileSuffix, COPT, DEBUG);
    includesstr = strcat(includesstr, ' -I"', gslPath, '"');
    if (~isempty(modelSourceFolder))
        includesstr = strcat(includesstr,' -I"', modelSourceFolder, '"');
    end

    %% Recompile AMICI base files if necessary
    [objectStrAmici] = compileAmiciBase(amiciRootPath, objectFolder, objectFileSuffix, includesstr, DEBUG, COPT);
    objectsstr = [objectsstr, objectStrAmici];

    %% Model-specific files
    for j=1:length(funs)
        baseFileName = [modelname '_' strrep(funs{j}, 'sigma_', 'sigma')];
        cfun(1).(funs{j}) = sourceNeedsRecompilation(modelSourceFolder, modelObjectFolder, baseFileName, objectFileSuffix);
    end

    funsForRecompile = {};

    % flag dependencies for recompilation
    if(~isempty(cfun))
        if(isfield('J',cfun(1)))
            if(cfun(1).J)
                if(ismember('JBand',funs))
                    cfun(1).JBand = 1;
                end
            end
        end

        if(isfield('JB',cfun(1)))
            if(cfun(1).JB)
                if(ismember('JBandB',funs))
                    cfun(1).JBandB = 1;
                end
            end
        end

        if(isfield('JSparse',cfun(1)))
            if(cfun(1).JSparse)
                if(ismember('sxdot',funs))
                    cfun(1).sxdot = 1;
                end
            end
        end
        if(isfield('dxdotdp',cfun(1)))
            if(isfield(cfun(1),'dxdotdp'))
                if(cfun(1).dxdotdp)
                    if(ismember('sxdot',funs))
                        cfun(1).sxdot = 1;
                    end
                    if(ismember('qBdot',funs))
                        cfun(1).qBdot = 1;
                    end
                end
            end
        end
        funsForRecompile = funs(structfun(@(x) logical(x), cfun(1)));
        funsForRecompile = cellfun(@(x) strrep(x, 'sigma_', 'sigma'), funsForRecompile, 'UniformOutput', false);
    end

    if(numel(funsForRecompile))
        fprintf('ffuns | ');

        sources = cellfun(@(x) ['"' fullfile(modelSourceFolder,[modelname '_' x '.cpp']) '"'],funsForRecompile,'UniformOutput',false);
        sources = strjoin(sources,' ');

        eval(['mex ' DEBUG COPT ...
            ' -c -outdir "' modelObjectFolder '" ' ...
            sources ' ' ...
            includesstr ]);
        cellfun(@(x) updateFileHashSource(modelSourceFolder, modelObjectFolder, [modelname '_' x]),funsForRecompile,'UniformOutput',false);
    end

    % append model object files
    for j=1:length(funs)
        filename = fullfile(modelObjectFolder, [modelname '_' strrep(funs{j}, 'sigma_', 'sigma') objectFileSuffix]);
        if(exist(filename,'file'))
            objectsstr = strcat(objectsstr,...
                ' "',filename,'"');
        end
    end

    % in case we compile python-generated code, there is a modelname.cpp
    model_cpp = fullfile(modelSourceFolder, [modelname '.cpp']);
    if(exist(model_cpp, 'file'))
        model_cpp = ['"' model_cpp '" '];
        model_cpp_obj = [' "' fullfile(modelObjectFolder,[modelname objectFileSuffix]) '" '];
    else
        model_cpp = '';
        model_cpp_obj = '';
    end


    % compile the wrapfunctions object
    fprintf('wrapfunctions | ');
    eval(['mex ' DEBUG COPT ...
        ' -c -outdir "' modelObjectFolder '" "' ...
        fullfile(modelSourceFolder,'wrapfunctions.cpp') '" ' model_cpp ...
        includesstr]);
    objectsstr = [objectsstr, ' "' fullfile(modelObjectFolder,['wrapfunctions' objectFileSuffix]) '" ' model_cpp_obj];

    % now we have compiled everything model-specific, so we can replace hashes.mat to prevent recompilation
    try
        movefile(fullfile(modelSourceFolder,'hashes_new.mat'),...
        fullfile(modelSourceFolder,'hashes.mat'),'f');
    end

    %% Linking
    fprintf('linking | ');

    if(isunix)
        if(~ismac)
            CLIBS = 'CLIBS="-lrt -lmwblas -ldl"';
        else
            CLIBS = 'CLIBS="-lmwblas"';
        end
    else
        if(strcmp(mex.getCompilerConfigurations('c++').Name,'MinGW64 Compiler (C++)'))
            CLIBS = 'LD="g++"';
        else
            CLIBS = '-lmwblas';
        end
    end

    mexFilename = fullfile(modelSourceFolder,['ami_' modelname]);
    eval(['mex ' DEBUG ' ' COPT ' ' CLIBS ...
        ' -output "' mexFilename '" ' objectsstr])
end

function [objectStrAmici] = compileAmiciBase(amiciRootPath, objectFolder, objectFileSuffix, includesstr, DEBUG, COPT)
    % generate hash for file and append debug string if we have an md5
    % file, check this hash against the contained hash
    cppsrc = {'amici', 'symbolic_functions','spline', ...
        'edata','rdata', 'exception', ...
        'interface_matlab', 'misc', 'simulation_parameters', ...
        'solver', 'solver_cvodes', 'solver_idas', 'model_state', ...
        'model', 'model_ode', 'model_dae', 'returndata_matlab', ...
        'forwardproblem', 'steadystateproblem', 'backwardproblem', 'newton_solver', ...
        'abstract_model', 'sundials_matrix_wrapper', 'sundials_linsol_wrapper', ...
        'vector'
    };
    % to be safe, recompile everything if headers have changed. otherwise
    % would need to check the full include hierarchy
    amiciIncludePath = fullfile(amiciRootPath,'include','amici');
    amiciSourcePath = fullfile(amiciRootPath,'src');
    recompile = headersHaveChanged(amiciIncludePath,objectFolder);
    objectArray = cellfun(@(x) [' "', fullfile(objectFolder, x), objectFileSuffix, '"'], cppsrc, 'UniformOutput', false);
    objectStrAmici = strjoin(objectArray, ' ');
    sourcesForRecompile = cppsrc(cellfun(@(x) recompile || sourceNeedsRecompilation(amiciSourcePath, objectFolder, x, objectFileSuffix), cppsrc));
    if(numel(sourcesForRecompile))
        fprintf('AMICI base files | ');
        sourceStr = '';
        for j = 1:numel(sourcesForRecompile)
            baseFilename = fullfile(amiciSourcePath, sourcesForRecompile{j});
            sourceStr  = [sourceStr, ' "', baseFilename, '.cpp"'];
        end
        eval(['mex ' DEBUG COPT ' -c -outdir "' objectFolder '" ' ...
            includesstr ' ' sourceStr]);
        cellfun(@(x) updateFileHashSource(amiciSourcePath, objectFolder, x), sourcesForRecompile);
        updateHeaderFileHashes(amiciIncludePath, objectFolder);
    end

end

function headersChanged = headersHaveChanged(includePath, objectFolder)
    list = dir([includePath '/*.h']);
    headersChanged = false;
    for file = {list.name}
        headersChanged = headerFileChanged(includePath, objectFolder, file{:});
        if(headersChanged)
            break;
        end
    end
end

function updateHeaderFileHashes(includePath, objectFolder)
    list = dir([includePath '/*.h']);
    for file = {list.name}
        updateFileHash(includePath, objectFolder, file{:});
    end
end


function hash = getFileHash(file)
    % getFileHash computed the md5hash of a given file
    %
    % Parameters:
    %  file: path of the file @type string
    %
    % Return values:
    %  hash: md5 hash of the provided file @type string
    hash = CalcMD5(file,'File','hex');
end

function updateFileHashSource(sourceFolder,objectFolder,baseFilename)
    fileName = [baseFilename '.cpp'];
    if(exist(fullfile(sourceFolder,fileName), 'file'))
        updateFileHash(sourceFolder,objectFolder,fileName);
    end
end


function updateFileHash(fileFolder,hashFolder,filename)
    hash = getFileHash(fullfile(fileFolder,filename));
    fid = fopen(fullfile(hashFolder,[filename '.md5']),'w');
    fprintf(fid,hash);
    fclose(fid);
end

function headerChanged = headerFileChanged(includePath, objectFolder, fileName)
    % headerFileChanged checks whether fileName.h  has changed since last compilation
    %
    % Parameters:
    %  * includePath: path to directory containing header file @type string
    %  * objectFolder: path to directory containing compiled md5 file @type string
    %  * fileName: name of header @type string
    %
    % Return values:
    %  headerChanged: flag indicating whether we need to recompile filestr.cpp
    hashFileSufffix = ['.md5'];
    headerFile = fullfile(includePath,fileName);
    hashFile = fullfile(objectFolder,[fileName hashFileSufffix]);
    if(~exist(headerFile , 'file') && exist(hashFile, 'file'))
        % header file has been removed, recompile and remove stray
        % hash file
        headerChanged = true;
        delete(hashFile);
    elseif(~exist(hashFile, 'file'))
        % there exists a header file but no corresponding hash file
        headerChanged = true;
    else
        % hash file exist, did hash change?
        headerChanged = hashHasChanged(headerFile, hashFile);
    end
end

function recompile = sourceNeedsRecompilation(amiciSourcePath, objectFolder, fileName, o_suffix)
    % sourceNeedsRecompilation checks whether fileName.cpp  has already been
    % compiled as fileName.o and whether the md5 hash of fileName.cpp matches
    % the one in fileName.md5
    %
    % Parameters:
    %  * amiciSourcePath: path to directory containing source file @type string
    %  * objectFolder: path to directory containing compiled object and md5 file @type string
    %  * fileName: name of source @type string
    %  * o_suffix: OS specific suffix for compiled objects @type string
    %
    % Return values:
    %  recompile: flag indicating whether we need to recompile filestr.cpp

    sourceFile = fullfile(amiciSourcePath,[fileName '.cpp']);

    if(~exist(sourceFile,'file'))
        % cpp does not exist, we don't need to compile :)
        recompile = 0;
    elseif(~exist(fullfile(objectFolder,[fileName o_suffix]),'file'))
        % object file does not exist, we need to recompile
        recompile = 1;
    else
        hashFile = fullfile(objectFolder,[fileName '.cpp.md5']);
        if(~exist(hashFile, 'file'))
            % source file hash does not exist, we need to recompile
            recompile = 1;
        else
            % hash files exists, did they change?
            recompile = hashHasChanged(sourceFile, hashFile);
        end
    end
end

function hasChanged = hashHasChanged(sourceFilename, hashFilename)
    % checkHash checks whether the given file matches the saved hash
    %
    % Parameters:
    %  * sourceFilename: the file to hash (has to exist)
    %  * hashFilename: the file where the hash is saved (has to exist)
    % Return values:
    %  * hasChanged: true if file and saved hash do not match

    hash = getFileHash(sourceFilename);
    fid = fopen(hashFilename);
    tline = fgetl(fid);
    fclose(fid);
    hasChanged = ~strcmp(tline, hash);
end

function versionstring = getCompilerVersionString()
    [~,systemreturn] = system([mex.getCompilerConfigurations('c++').Details.CompilerExecutable ' --version']);
    newlinePos = strfind(systemreturn, sprintf('\n'));
    str = systemreturn(1:(newlinePos(1)-1));
    str = regexprep(str,'[\(\)]','');
    str = regexprep(str,'[\s\.\-]','_');
    versionstring = genvarname(str); % fix everything else we have missed
end