cosmo publish run scripts skl

function is_ok = cosmo_publish_run_scripts(varargin)
    % helper function to publish example scripts (for developers)
    %
    % cosmo_publish_build_html([force|fn])
    %
    % Inputs:
    %   fn           filename of matlab file to publish (in the examples
    %                directory)
    %   '-force'     force rebuild, even if an output file is newer than the
    %                corresponding output file
    %   '-dry'       dry run: do not build any files, but show the output that
    %                would be shown.
    %   '-o', d      write output in directory d. By default d is
    %                'doc/source/_static/publish/' relative to the CoSMoMVPA
    %                root directory.
    %
    % Notes:
    %  - if no filename is given, then all files are rebuilt if necessary
    %  - this function is intended for developers (to build the website)
    %  - whether an output file is out of date is first determined using
    %    git: if git shows no changes to the last commit of the input file
    %    then it is assumed it is not out of date. If the input file has
    %    been changed since the last commit (or never been committed), then
    %    the modification date is used to determine whether it is out of date.
    %  - requirements
    %    * a Unix-like system
    %    * a working installation of git
    %    * CoSMoMVPA code present in a git repository
    %
    % #   For CoSMoMVPA's copyright information and license terms,   #
    % #   see the COPYING file distributed with CoSMoMVPA.           #

    % ensure 'publish' function is available
    cosmo_check_external('!publish', true);

    [srcfn_cell, opt] = process_input(varargin{:});
    trgdir = get_output_dir(opt);

    trgext = ['.' opt.format];
    summaryfn = ['index.' opt.format];

    orig_path = path();
    path_resetter = onCleanup(@()path(orig_path));

    % go to mvpa dir
    orig_pwd = pwd();
    pwd_resetter = onCleanup(@()cd(orig_pwd));
    mvpa_dir = fileparts(mfilename('fullpath'));
    cd(mvpa_dir);

    if ~isdir(trgdir) && ~opt.dryrun
        mkdir_recursively(trgdir);
    end

    nsrc = numel(srcfn_cell);
    outputs = cell(nsrc, 1);

    total_time_took = 0;
    output_pos = 0;
    is_ok = true;

    for k = 1:nsrc
        srcfn = srcfn_cell{k};
        [srcpth, srcnm] = fileparts(srcfn);

        if k == 1
            addpath(srcpth);
        end

        trgfn = fullfile(trgdir, [srcnm trgext]);

        [needs_update, build_msg] = target_needs_update(srcfn, trgfn);
        if opt.force
            build_msg = sprintf('update forced: %s', srcfn);
        end

        fprintf(build_msg);

        do_update = needs_update || opt.force;

        if do_update
            fprintf('\n   building ... ');
            clock_start = clock();

            if opt.dryrun
                fprintf('<dry run>');
                is_built = true;
            else
                is_built = publish_helper(srcfn, trgfn);
            end

            clock_end = clock();
            time_took = etime(clock_end, clock_start);
            total_time_took = total_time_took + time_took;

            if is_built
                outcome_msg = ' done';
            else
                outcome_msg = ' !! failed';
                is_ok = false;
            end

            status_msg = sprintf('%s (%.1f sec)', outcome_msg, time_took);
        else
            status_msg = '';
        end

        fprintf('%s\n', status_msg);

        output_pos = output_pos + 1;
        outputs{output_pos} = srcnm;
    end

    fprintf('Processed %d files (%.1f sec)\n', output_pos, total_time_took);

    outputfn = fullfile(trgdir, summaryfn);

    if ~opt.dryrun
        write_index(outputfn, opt);
    end

function write_index(index_outputfn, opt)
    [outputdir, outputnm] = fileparts(index_outputfn);
    inputdir = fullfile(get_root_dir(), 'examples');

    d = dir(fullfile(inputdir, '*.m'));

    fid = fopen(index_outputfn, 'w');
    cleaner3 = onCleanup(@()fclose(fid));
    fprintf(fid, ['<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN"'...
                  '>\n']);
    fprintf(fid, ['<HTML><HEAD><TITLE>Index of matlab outputs'...
                  '</TITLE></HEAD>\n<BODY>Matlab output<UL>\n']);

    for k = 1:numel(d)
        [unused, nm] = fileparts(d(k).name);
        outputfn = sprintf('%s.%s', nm, opt.format);
        if exist(fullfile(outputdir, outputfn), 'file')
            fprintf(fid, '<LI><A HREF="%s">%s</A></LI>\n', outputfn, nm);
        end
    end

    fprintf(fid, ['</UL>Back to <A HREF="../../%s.html">index'...
                  '</A>.</BODY></HTML>\n'], outputnm);

    fprintf('Index written to %s\n', index_outputfn);

function trgdir = get_output_dir(opt)
    trgdir = opt.output_dir;
    if isempty(trgdir)
        trgdir = fullfile(get_root_dir(), 'doc/source/_static/publish/');
    end

function d = get_root_dir()
    d = fileparts(fileparts(mfilename('fullpath')));

function is_built = publish_helper(srcfn, trgfn)
    is_built = false;
    try
        publish_wrapper(srcfn, trgfn);
        is_built = true;
    catch
        me = lasterror();

        if exist(trgfn, 'file')
            delete(trgfn);
        end

        msg = sprintf('Unable to build %s: %s\n', srcfn, ...
                      me.message);

        s = me.stack;
        for j = 1:numel(s)
            msg = sprintf('%s\n  %s:%s', msg, s(j).file, s(j).line);
        end

        cosmo_warning('%s', msg);
    end

function publish_wrapper(srcfn, trgfn)
    [srcdir, srcnm] = fileparts(srcfn);
    trgdir = fileparts(trgfn);

    orig_pwd = pwd();
    pwd_resetter = onCleanup(@()cd(orig_pwd));

    orig_fig_handles = findobj('Type', 'figure');
    fig_cleaner = onCleanup(@()close(setdiff(findobj('Type', 'figure'), ...
                                             orig_fig_handles)));

    addpath(srcdir);
    cd(trgdir);

    if cosmo_wtf('is_matlab')
        args = {struct('outputDir', trgdir, 'catchError', false)};
    else
        args = {'format', 'html', 'imageFormat', 'jpg'};
    end

    publish(srcnm, args{:});

function [srcfn_cell, opt] = process_input(varargin)
    % set defaults
    srcpat = '';

    opt = struct();
    opt.force = false;
    opt.dryrun = false;
    opt.format = 'html'; % currently not changeable
    opt.output_dir = '';

    % process arguments
    n = numel(varargin);

    k = 0;
    while k < n
        k = k + 1;
        arg = varargin{k};
        if ischar(arg)
            if strcmp(arg, '-force')
                opt.force = true;
            elseif strcmp(arg, '-dry')
                opt.dryrun = true;
            elseif strcmp(arg, '-o')
                if k == n
                    error('Missing argument after %s', arg);
                end
                k = k + 1;
                opt.output_dir = varargin{k};

            elseif ~isempty(srcpat)
                error('multiple inputs found, this is not supported');
            else
                srcpat = arg;
            end
        else
            error('Illegal argument at position %d - expected string', k);
        end
    end

    srcfn_cell = get_srcfn_cell(srcpat);

function srcfn_cell = get_srcfn_cell(srcpat)
    default_pattern = {'run_*', 'demo_*'};

    if isdir(srcpat)
        p = srcpat;
        nms = default_pattern;
        e = '.m';
    else
        [p, nm, e] = fileparts(srcpat);
        if isempty(p)
            p = fullfile(get_root_dir(), 'examples');
        end

        if isempty(nm)
            nms = default_pattern;
        else
            nms = {nm};
        end

        if isempty(e)
            e = '.m';
        end
    end

    ds = cellfun(@(nm)dir(fullfile(p, [nm e])), nms, 'UniformOutput', false);
    d = cat(1, ds{:});
    n = numel(d);
    if n == 0
        error('No input files found');
    end
    srcfn_cell = cellfun(@(fn)fullfile(p, fn), {d.name}, 'UniformOutput', false);

function [tf, msg] = target_needs_update(srcfn, trgfn)
    % helper function to see if html is out of date
    [unused, root, srcext] = fileparts(srcfn);
    [unused, root_alt, trgext] = fileparts(trgfn);
    assert(isequal(root, root_alt));

    srcname = [root, srcext];
    trgname = [root, trgext];
    t_trg = time_last_changed(trgfn);
    if isnan(t_trg)
        % does not exist, so needs update
        tf = true;
        msg = sprintf('not found: %s', trgname);
        return
    end

    if is_in_staging(srcfn) || is_untracked(srcfn)
        % changes since last commit, see when changes were made
        t_src = time_last_changed(srcfn);
        tf = isnan(t_src) || t_trg < t_src;
        msg = sprintf('modified: %s', srcname);
    else
        % no changes since last commit, see when last commit was made
        t_src = time_last_commit(srcfn);
        tf = isnan(t_src) || t_trg < t_src;
        msg = sprintf('recent commit: %s', srcname);
    end

    if ~tf
        msg = sprintf('up to date: %s', trgname);
    end

function t = time_last_changed(fn)
    d = dir(fn);
    if isempty(d)
        t = NaN;
        return
    end
    assert(numel(d) == 1);
    t = (d.datenum - datenum(1970, 1, 1)) * 86400;

function r = run_git(args)
    prefix = 'export TERM=ansi; git ';
    cmd = [prefix args];

    [e, r] = unix(cmd);

    if e
        fprintf(2, 'Unable to run git, or an error was produced\n');
        error(r);
    end

function tf = is_untracked(srcfn)
    untracked = run_git('ls-files . --exclude-standard --others');
    tf = cosmo_match({srcfn}, untracked);

function t = time_last_commit(srcfn)
    cmd = sprintf('log -n 1 --pretty=format:%%ct -- %s', srcfn);
    r = run_git(cmd);

    t = str2double(regexp(r, '(\d*)', 'match', 'once'));

    if isempty(t)
        t = NaN;
    end
    assert(numel(t) == 1);

function tf = is_in_staging(fn)
    in_staging_str = run_git('diff HEAD  --name-only | xargs basename');
    in_staging = cosmo_strsplit(in_staging_str, '\n');

    basefn = cosmo_strsplit(fn, filesep, -1);
    tf = cosmo_match({basefn}, in_staging);

function mkdir_recursively(trgdir)
    parent = fileparts(trgdir);
    if ~isdir(parent)
        mkdir_recursively(parent);
    end
    mkdir(trgdir);