image thumbnail

Deploy pcoded Matlab Files

by

 

01 Mar 2013 (Updated )

Creates recursively p-coded files and the associated help text, works also for classes and packages

deploy.m
function deploy( source, destination, varargin)
% DEPLOY creates p-coded files and the associated help text of a given
% source to a given destination folder. Works recursive on folder trees,
% including Package folders (+Package) and classes as well as class folders
% (@Class). For classes no help is created for the private functions in the
% private folder, but help is also created for all public get / constant
% properties.
%
%
% Syntax: deploy( source, destination, varargin)
%
% Inputs:
%   -source:        Can be a single M-file or a folder, also works for
%                   packages and Classes
%   -destination:   Has to be an empty folder
%   -varargin:      The folowing options can be added:
%                   'excludes'      - the files to exclude, always excluded
%                                     is this file itself
%                   'includes'      - the additional files to deploy, e.g.
%                                     {'*.pdf', '*.png'}
%                   'recursive'     - default is true, can be set to false.
%                                     Packages and Class directories are
%                                     always included and cannot be
%                                     excluded
%                   'includeDLL'    - Dll's are by default included. To
%                                     exclude Dll's set this option to false 
%                   'includeMex'    - Mex files are by default included, To
%                                     exclude Mex files, set this option to
%                                     false
%                   'includeHiddenDirs    - by default . presceded folders
%                                           and files are excluded by default, 
%                                           set this option to true to
%                                           include . preceded folders,
%                                           e.g. .svn or .git
%                   'purgeDestination'    - ATTENTION, if this option is
%                                           set to true, the destination
%                                           will be deleted and all its
%                                           subdirectories without further
%                                           warning.
%
% Outputs:
%   -none
% Example:
%   deploy( 'myfun.m', 'for_public' ); % where myfun.m is a function and
%                                        for_public a folder
%   deploy( 'myproject', 'for_public' ); % where myproject is a folder
%                                           and for_public a folder
%   deploy('myproject', 'project_v1.0', 'recursive', true, ...
%           'includeHiddenDirs', true, 'purgeDestination', true, ...
%           'includes', {'*.pdf', '*.png'});
%
% Other m-files required:
%   None
%
% Other files required:
%   MATLAB
%
% See also: pcode

% About and copyright
% Author: Adrian Etter
% http://www.econ.uzh.ch/faculty/etter.html
% E-Mail: etteradrian@gmail.com
%  Department of Economics,
% University of Zurich
% Version 1.51 2013/June/26
% Last changes:
%   2013/June/26
%   - destination was created before checked if there were any files to
%     process
%   - fixed a bug that made that single files couldnt be processed
%   - fixed a bug that caused, that when deploying @class as direct source,
%     the @class folder structure wasn't kept.
%   - fixed a bug that caused, that when deploying +package as direct source,
%     the +package folder structure wasn't kept.
%   2013/June/19
%   - added recursion for subfolders
%   - added support for Matlab Classes
%   - added support for Matlab ClassFolders
%   - added support for Matlab PackageFolders
%   2013/Mar/01
%   - finished
% 

% CHECK SOURCE
    if isempty(source)
        source = '.';
    end
    assert(ischar(source), 'deploy:source', 'Source must be string, see help!');
    
    flags = processFlags(varargin{:});
        
    % CHECK DESTINATION
    assert(ischar(destination), 'deploy:destination', 'Destination must be string, see help!');
    assert(~isempty(destination), 'deploy:destination', 'Destination variable cannot be empty, see help!');
    

    % CREATE DESTINATION IF IT NOT ALREADY EXISTS
    dirNew = false;
    if isdir(destination)
        if flags.purgeDestination
            rmdir(destination, 's');
            mkdir(destination);
        else
            assert(numel(dir(destination)) < 3, ...
                'deploy:destination:notempty', ...
                'Destination Folder is not empty! Destination folder has to be empty!');
        end
    else
        mkdir(destination);
        dirNew = true;
    end     
    destination = [cd(cd(destination)), filesep];
    
    % GET FILES TO PROCESS   
    if flags.verbose
        disp('Generating files to process tree');
    end
    
    if isdir(source)        
        source = [cd(cd(source)) filesep];
    end
    
    % IF SOURCE IS A PACKAGE, WE WANT TO KEEP THAT STRUCTURE
    addPackToDest = false;
    package = getPackageName(source);
    destPackage = getPackageName(destination);
    if ~isempty(package) 
        if isempty(destPackage)
            addPackToDest = true;
        else
            assert(strcmp(package, destPackage), ...
                'deploy:source:package:missmatch', ...
                'Source and destination folders are packages, but with different names. You cannot rename while deploying!')
        end
    end
    
    addClassToDest = false;
    class = getClassName(source);
    destClass = getClassName(destination);
    if ~isempty(class) 
        if isempty(destClass)
            addClassToDest = true;
        else
              assert(strcmp(class, destClass), ...
                'deploy:source:class:missmatch', ...
                'Source and destination folders are packages, but with different names. You cannot rename while deploying!')
        end
    end
    
    if addPackToDest
        destination = cat(2, destination, filesep, '+', package);
    end
    
    if addClassToDest
        destination = cat(2, destination, filesep, '@', class);
    end
    
    
    files = generateFileProcessList(source, destination, flags); 
    if isempty(files) % nothing to do!
        disp('No files to process found!');
        if dirNew
            rmdir(destination, 's');
        end
        return;
    end    
    

    
    % CREATE DESTINATION FOLDER TREE
    for i = 1:numel(files)
        if ~isdir(files(i).destination)
            mkdir(files(i).destination);
        end
    end
    if flags.verbose
        disp('Processing files... this could take a while!');
    end
    processFiles(files, flags);
end

function [ flags ] = processFlags( varargin )

        defaultExcludes                 = {[mfilename '.m']}; %{'*.pdf'; '*.png'};
        defaultRecursive                = true;
        parentClassFolder               = false;
        defaultIncludes                 = {'*.m'};
        mexIncludes                     = cellfun(@(y) ['*.' y], feval(@(x) {x.ext}.' , mexext('all')), 'UniformOutput', false);
        dllIncludes                     = {'*.dll'};
        defaultIncludeDLL               = true;
        defaultIncludeMex               = true;
        defaultIncludeHiddenFolders     = false;
        defaultPurgeDestination         = false;
        defaultVerbose                  = false;
        
        
        p = inputParser;
        p.addParamValue('excludes', defaultExcludes,  @(x) (ischar(x) || iscellstr(x)));
        p.addParamValue('includes', defaultIncludes, @(x) (ischar(x) || iscellstr(x)));
        p.addParamValue('recursive', defaultRecursive, @(x) (islogical(logical(x)) && isscalar(x)));
        p.addParamValue('includeDLL', defaultIncludeDLL, @(x) (islogical(logical(x)) && isscalar(x)));
        p.addParamValue('includeMex', defaultIncludeMex, @(x) (islogical(logical(x)) && isscalar(x)));
        p.addParamValue('includeHiddenDirs', defaultIncludeHiddenFolders, @(x) (islogical(logical(x)) && isscalar(x)));
        p.addParamValue('purgeDestination', defaultPurgeDestination, @(x) (islogical(logical(x)) && isscalar(x)));
        p.addParamValue('verbose', defaultVerbose, @(x) (islogical(logical(x)) && isscalar(x)));
        p.addParamValue('parentClassFolder', parentClassFolder);
        
        p.parse(varargin{:});
        flags = p.Results();        
        flags.recursive = logical(flags.recursive);
        flags.includeDLL = logical(flags.includeDLL);
        flags.includeMex = logical(flags.includeMex);
        flags.includeHiddenDirs = logical(flags.includeHiddenDirs);
        flags.verbose = logical(flags.verbose);
        
        
        if ischar(flags.excludes)
            flags.excludes = {flags.excludes};
        end
        if ischar(flags.includes)
            flags.includes = {flags.includes};
        end
        
        flags.excludes = unique(cat(1, defaultExcludes, flags.excludes{:}));
        flags.includes = unique(cat(1, defaultIncludes, flags.includes{:}));
        
        if flags.includeDLL
            flags.includes = cat(1, flags.includes, dllIncludes);
        end
        
        if flags.includeMex
            flags.includes = cat(1, flags.includes, mexIncludes);
        end        
        
        if ~flags.includeHiddenDirs
            flags.excludes = cat(1, flags.excludes, '.*');
        end       
        
        assert(isempty(intersect(flags.includes, flags.excludes)), ...
            'deploy:includeexclude:intersect', 'Includes and Excludes intersect, you cannot exclude and include the same option!');

end

function processFiles(files, flags)    
        for i = 1 : numel(files)   
            if flags.verbose
                disp(files(i).fullpath);
            end
            if files(i).isMFile
                if files(i).generateHelp                  
                    writeHelpFile(files(i));
                end
                makePCode(files(i));
            else % no mfile, so just make a copy of the file
                [success, msg, ~] = copyfile(files(i).fullpath, files(i).destination,'f');
                assert(success == 1, ...
                'deploy:copyfile:failed', ...
                'Copy file failed and interrupted on file: %s. Message: %s', files(i).fullpath, msg);
            end
            

        end    
end

function makePCode(file)
    actualpath = path();
    path(file.base, actualpath);
    pcode(file.fullpath, '-INPLACE');
    path(actualpath);
    pfile = [file.fullpath(1:end-1) 'p'];
    [success, ~, ~] = movefile(pfile, file.destination, 'f');
    assert(success == 1, ...
        ['Move file failed and interrupted on file: ' pfile]);
end

function writeHelpFile(file)

    actualpath = path();
    path(file.base, actualpath);
    try
        if isempty(meta.class.fromName(file.helpCmd)) % normal file
            writeFunctionHelp(file);
        else % is a class
            writeClassHelp(file);
        end
        path(actualpath);
    catch err
        path(actualpath);
        rethrow(err);
    end
end

function txt = getHelpText(cmdName)
    txt = help(cmdName);
    if isempty(txt)
        return;
    end
    txt = regexp(txt, '\n','split').';
    txt = strcat('%', txt);
end

function writeClassHelp(file)
    
    % CREATING CLASS HEADER
    helptext = getHelpText(file.helpCmd);
    if isempty(helptext)
        return;
    end   
    % ADD classdef line to TOP
    headLine = getHeadLine(file, 'classdef');
    if ~isempty(headLine)
        helptext = vertcat(headLine, helptext);
    end
    
    % GETTING CLASS META INFORMATION
    metainfo = meta.class.fromName(file.helpCmd);
    % PROCESSING PUBLIC GET PROPERTIES
    properties = metainfo.PropertyList(strcmp({metainfo.PropertyList.GetAccess}, 'public') & [metainfo.PropertyList.Hidden] == false);
    helptext  = vertcat(helptext, sprintf('\n\tproperties\n'));
    for i = 1:numel(properties)
        propertyHelp = getHelpText([file.helpCmd '.' properties(i).Name]);
        if ~isempty(propertyHelp)
            propertyHelp = sprintf('\t\t%s; %%%s\n', properties(i).Name, [propertyHelp{:}]);
            helptext  = vertcat(helptext, propertyHelp); %#ok<AGROW> Yes it's growing, ans we have no idea how much, so no preallocating!
        end
    end
    helptext  = vertcat(helptext, sprintf('\n\tend\n'));
    % PROCESSING PUBLIC METHODS    
    df = [metainfo.MethodList.DefiningClass];
    methods = metainfo.MethodList(strcmp({metainfo.MethodList.Access}, 'public') & strcmp({df.Name}, file.helpCmd));
    helptext  = vertcat(helptext, sprintf('\n\tmethods\n'));
    for i = 1:numel(methods)
        methodHelp = getHelpText([file.helpCmd '.' methods(i).Name]);
        if ~isempty(methodHelp)
            functionSignature = sprintf('\t\tfunction');
            
            % HANDLE OUTPUT ARGUMENTS
            if numel(methods(i).OutputNames) > 0
                functionSignature = sprintf('%s [', functionSignature);
                for j = 1:numel(methods(i).OutputNames) - 1
                    functionSignature = sprintf('%s%s, ', functionSignature, methods(i).OutputNames{j});
                end
                functionSignature = sprintf('%s%s] =', functionSignature, methods(i).OutputNames{end});
            end            
            % ADD FUNCTION NAME
            functionSignature = sprintf('%s %s(', functionSignature, methods(i).Name);
            % HANDLE INPUT ARGUMENTS
            if numel(methods(i).InputNames) > 0
                for j = 1:numel(methods(i).InputNames) - 1
                    functionSignature = sprintf('%s%s, ', functionSignature, methods(i).InputNames{j});
                end
                functionSignature = sprintf('%s%s', functionSignature, methods(i).InputNames{end});
            end
            functionSignature = sprintf('%s)', functionSignature);
            methodHelp = cellfun(@(x) sprintf('\t\t\t%s', x), methodHelp, 'UniformOutput', false);
            helptext  = vertcat(helptext, functionSignature, methodHelp, sprintf('\n\t\tend\n')); %#ok<AGROW> Yes it's growing, ans we have no idea how much, so no preallocating!
        end
    end
    helptext  = vertcat(helptext, sprintf('\n\tend\n'));
    % ADDING END TO CLASSDEF
    helptext  = vertcat(helptext, sprintf('\nend\n'));    
    writeFile(file, helptext);
end

function writeFunctionHelp(file)
    helptext = getHelpText(file.helpCmd);
    if isempty(helptext)
        return;
    end
    
    % get function Signature Line
    headLine = getHeadLine(file, 'function');
    if ~isempty(headLine)
        helptext = vertcat(headLine, helptext);
    end

    writeFile(file, helptext);
end


function headLine = getHeadLine(file, keyword)
    fID = fopen(file.fullpath, 'r');
    assert(fID ~= -1, 'DEPLOY:writeHelpFile:getHeadLine', 'Could not get headline because, could not read input: %s!', file.fullpath);
    headLine = '';
    while ~feof(fID)
        line = strtrim(fgetl(fID));
        if strcmp(line(1:min(length(line), length(keyword))), keyword)
            % headline is found return
            headLine = line;
            break;
        end
    end
    fclose(fID);
end

function writeFile(file, text)
    outFilename = [file.destination file.name];
    fid = fopen(outFilename,'w');
    assert(fid ~= -1, 'DEPLOY:writeHelpFile', 'Write helpfile failed, check write permission on destination!');
    fprintf(fid, '%s\n',text{:});
    fclose(fid);
end

function files = generateFileProcessList(source, destination, flags)
    [~, ~, extension] = fileparts(source);
    if ~strcmp(extension, '.m') && ~strcmp(source(end), filesep) % % not single file AND not ending on a filesep
        source = [source filesep];
    end
    if ~strcmp(destination(end), filesep)
        destination = [destination filesep];
    end
    dircontent = dir(source);    
    
    % EXCLUDE LIST
    if numel(dircontent) > 1 % not only a single file
        excludes = cellfun(@(x) feval(@(y) {y.name}, dir([source x])), flags.excludes, 'UniformOutput', false); % a list of all files that are in the ignore list
    else
        excludes = cellfun(@(x) feval(@(y) {y.name}, dir([x])), flags.excludes, 'UniformOutput', false); %#ok<NBRAK> % a list of all files that are in the ignore list
    end
    excludes = [excludes{:}, '.', '..'].';
    
    % INCLUDE LIST    
    if numel(dircontent) > 1 % not only a single file
        includes = cellfun(@(x) feval(@(y) {y.name}, dir([source x])), flags.includes, 'UniformOutput', false); % a list of all files that are in the ignore list    
        includes = [includes{:}].';
    else
        includes = source;
    end
    
    dircontent      = dircontent(logical(cellfun(@(x) ~any(strcmp(x, excludes)), {dircontent.name}).')); % first remove the ones from the ignore list
    directories     = dircontent(logical([dircontent.isdir] == 1));
    includefiles    = dircontent(logical(cellfun(@(x) any(strcmp(x, includes)), {dircontent.name}).')); % remove the ignore list from the list of files to process
    dircontent      = cat(1, directories, includefiles);
    
    
    files = struct('name', {}, 'source', {}, 'destination', {}, 'fullpath', {}, 'isMFile', {}, 'base', {}, 'helpCmd', {}, 'generateHelp', {});
    for i = 1:numel(dircontent)
        if dircontent(i).isdir % if is directory and one of the below conditions    
            if flags.recursive ... recursive
                    || strcmp(dircontent(i).name(1), '+') ... packages will always be recursivly handled
                    || strcmp(dircontent(i).name(1), '@') ... starts with an @
                    || (flags.parentClassFolder && strcmp(dircontent(i).name, 'private')) % parent folder started with an @ and now we  need to process the private folder
                if strcmp(dircontent(i).name(1), '@')
                    flags.parentClassFolder = true;
                else
                    flags.parentClassFolder = false;
                end
                pfiles = generateFileProcessList([source dircontent(i).name filesep], [destination dircontent(i).name filesep], flags);
                files = cat(1, files, pfiles);
            end
        else % not directory, a file to process
            if ~mislocked(dircontent(i).name)
                file.name           = dircontent(i).name;
                if strcmp(file.name, source)
                    file.source     = [pwd filesep];
                else
                    file.source     = source;
                end
                file.destination    = destination;
%                 destPath            = '';
                file.fullpath       = [file.source file.name];
                file.isMFile        = false;
                file.base           = file.source;
                file.helpCmd        = file.name;
                file.generateHelp   = false;
                
                [~, name, extension] = fileparts(file.fullpath);
                % IF WE HAVE AN mfile we need some further investigations:
                if strcmp(extension, '.m') % check if it's an m-file
                    file.isMFile        = true;
                    file.generateHelp   = true; % normaly generate help file, turning it off on specific files
                    
                    basePath            = file.source;
                    
                    helpCmd             = name;
                    
                    
                    privatePattern      = sprintf('\\%s@(\\w*)\\%sprivate\\%s', filesep, filesep, filesep);
                    
                    % CHECK IF IS A PRIVATE METHOD OF A CLASS FOLDER
                    if ~isempty(regexp(file.source, privatePattern, 'ONCE'))
                        file.generateHelp   = false;
                    end
                    
                    % CHECK IF IS A CLASS FOLDER, FLAG CLASS FILE FOR
                    % PROCESSING
                    class = getClassName(file.source);
%                     if ~isempty(regexp(file.source, classpattern, 'ONCE')) % path containing a @class folder
%                         class = regexp(file.source, classDirPattern, 'match');
                    if ~isempty(class)
                        if ~strcmp(name, class) % is public method
                            file.generateHelp   = false;
                        end
%                         className   = class{:};
                        basePath    = strrep(basePath, ['@' class filesep], '');
                        helpCmd     = class;
%                         if isempty(regexp(destination, classpattern, 'ONCE'))
%                             destPath    = cat(2, destPath, '@', className, filesep);
%                         end
                    end
                    
                    % CHECK IF IS A PACKAGE FOLDER, FLAG PACKAGE FILE FOR
                    % PROCESSING
                    package = getPackageName(file.source);
                    if ~isempty(package)
%                     if ~isempty(regexp(file.source, packagepattern, 'ONCE')) % path containing a +package Folder
%                         package = regexp(file.source, packageDirPattern, 'match');
                        basePath        = strrep(basePath, ['+' package filesep], '');
                        helpCmd         = [package '.' helpCmd]; %#ok<AGROW> Not true, helpCmd is reseted on line 78;
%                         if isempty(regexp(destination, packagepattern, 'ONCE'))
%                             destPath    = cat(2, '+', packageName, filesep, destPath);
%                         end
                    end
                    file.helpCmd        = helpCmd;
                    file.base           = basePath;                    
                end
%                 file.destination        = cat(2, destination, filesep, destPath);
                files = cat(1, files, file);
            else
                warning('deploy:generateFileProcessList:lockedFile', ...
                    ['File: ' source dircontent(i).name ' was not added, because file is locked!']);
            end
        end
    end

end

function packageName = getPackageName(path)
    packageDirPattern   = sprintf('(?<=\\+)(.*?)(?=\\%s)', filesep);
    packageName = regexp(path, packageDirPattern, 'match');
    if ~isempty(packageName)
        packageName = packageName{:};
    end
end

function className = getClassName(path)
    classDirPattern     = sprintf('(?<=@)(.*)(?=\\%s)', filesep);    
    className = regexp(path, classDirPattern, 'match');
    if ~isempty(className)
        className = className{:};
    end
end

Contact us