cmdlets/ArXiv.ps1

#!/usr/bin/env powershell
##
# ArXiv.ps1: Cmdlets for building arXix-ready ZIP archives.
##
# © 2017 Christopher Granade (cgranade@cgranade.com)
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are
# met:
#
# 1. Redistributions of source code must retain the above copyright notice,
# this list of conditions and the following disclaimer.
#
# 2. Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
#
# 3. Neither the name of PoShTeX nor the names
# of its contributors may be used to endorse or promote products derived
# from this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
# PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
# HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
##

## COMMANDS ##

function Expand-ArXivManifest {
    # [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true)] [hashtable] $Manifest
    );

    # Begin by making a new hashtable to accumulate build files.
    # Each entry in the hashtable will identify a particular file
    # and its destination within the eventual ZIP file.
    $buildFiles = @{};

    # Put the TeXMain file in the root by isolating its basename
    # (that is, without the path) and making that the destination
    # for the actual file pointed to by TeXMain.
    $texMain = $Manifest["TeXMain"];
    $texMainLocation = Split-Path $texMain;
    if ($texMainLocation.Length -eq 0) {
        $texMainLocation = ".";
    }
    $texMainBase = Split-Path $texMain -Leaf;
    $texMainDest = Join-Path "." $texMainBase;
    $buildFiles[($texMain | Resolve-Path)] = $texMainDest;
    
    # Next, add the BBL.
    $bblBase = set-extension -Path $texMainBase -newExtension "bbl";
    $buildFiles[(
        Join-Path -Path $texMainLocation -ChildPath $bblBase | Resolve-Path
    )] = Join-Path "." $bblBase;

    # Next, loop through the AdditionalFiles and add each to
    # the build files accordingly.
    $Manifest["AdditionalFiles"].GetEnumerator() | % {
        $glob = $_.Key;
        $targetDir = $_.Value;

        if (!($targetDir)) {
            # Check if the target is left implicit. If so, resolve
            # the glob and add each.
            Resolve-Path $glob -Relative | % {
                $buildFiles[(Resolve-Path $_)] = $_;
            } | Out-Null
        } elseif ($targetDir.EndsWith("/") -or $targetDir.EndsWith("\")) {
            # Next, check if the target is a directory. If so, each
            # item matching the glob gets added to the same directory.
            Resolve-Path $glob -Relative | % {
                $buildFiles[(Resolve-Path $_)] = (Join-Path $targetDir -ChildPath (Split-Path $_ -Leaf));
            } | Out-Null
        } else { 
            # If we're here, then the target was an explicit name, so we
            # don't need to glob it up.
            $buildFiles[$glob] = $targetDir;
        }
    } | Out-Null

    # Add any Notebooks to anc/. We'll possibly rerun them in a later build
    # stage.
    $Manifest["Notebooks"] | % {
        $buildFiles[(Resolve-Path $_)] = Join-Path "./anc" (Split-Path -Leaf $_)
    } | Out-Null

    # Finish by attaching the hashtable of build files to the manifest
    # and returning it.
    $expandedManifest = $Manifest.Clone();
    $expandedManifest["DestinationTeXMain"] = $texMainDest;
    $expandedManifest["BuildFiles"] = $buildFiles;
    
    $expandedManifest

}

function Copy-ArXivArchive {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true)] [hashtable]
        $ExpandedManifest
    )

    $tempDirName = [System.IO.Path]::GetTempFileName();
    Remove-Item $tempDirName;
    New-Item -ItemType Directory -Path $tempDirName | Out-Null;

    Write-Host -ForegroundColor Blue "Copying arXiv build files to $tempDirName."

    $ExpandedManifest["BuildFiles"].GetEnumerator() | % {
        
        $target = Join-Path $tempDirName $_.Value;
        $targetDir = Split-Path $target;

        # Make the target directory if it doesn't exist.
        if (!(Get-Item $targetDir -ErrorAction SilentlyContinue)) {
            New-Item -ItemType Directory $targetDir
        }
        write-host "Copying $($_.Key) -> $target"
        Copy-Item $_.Key $target

    } | Out-Null;

    $tempDirName

}

function Export-ArXivArchive {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true)] [hashtable] $Manifest,
        [switch] $RunNotebooks
    );

    $ExpandedManifest = Expand-ArXivManifest $Manifest;

    # Invoke the default TeX build engine in the main project directory,
    # so that we can make all required TeX artifacts (e.g. *.bbl) from
    # files that shouldn't be included in the final arXiv build.
    Invoke-TeXBuildEngine $ExpandedManifest.TeXMain;

    if ($RunNotebooks -and $ExpandedManifest.Notebooks.Count -ge 0) {
        $ExpandedManifest.Notebooks | Update-JupyterNotebook
    }
    
    $tempDir = Copy-ArXivArchive $ExpandedManifest;
    $tempTexMain = Join-Path $tempDir $ExpandedManifest.DestinationTeXMain

    if ($ExpandedManifest["RenewCommands"]) {
        Write-Host -ForegroundColor Blue "Rewriting commands on temporary copy.";
        Update-LaTeXCommands $tempTexMain $ExpandedManifest["RenewCommands"];
    }

    # Invoke pdfLaTeX in the temporary directory to ensure that the rewritten commands
    # and copied build artifacts (e.g. *.bbl) are correctly utilized.
    Write-Host -ForegroundColor Blue "Invoking TeX on temporary copy: $tempTexMain."
    Push-Location $tempDir;
        # Since this build is only to verify that everything worked, we make
        # a list of all files currently in the temporary build directory.
        # We will delete anything that's not in this list at the end to prevent
        # *.aux and *.pdf files from leaking into the arXiv zip.
        $tempDirContents = Get-ChildItem -Recurse;

        # Note that we don't use Invoke-TeXBuildEngine here since we are
        # compiling to verify that things will work correctly when built by
        # arXiv. Their build process doesn't include bibtex or similar steps,
        # so we emulate here by only running pdflatex.
        & "pdflatex" $tempTexMain;
        & "pdflatex" $tempTexMain;

        # Before popping out of the temporary build directory, we need to clean
        # it up.
        # Use -Force to enable using posh-tex in non-interactive environments.
        $newContents = Get-ChildItem -Recurse;
        Compare-Object $tempDirContents $newContents `
            | Where-Object { $_.SideIndicator -eq "=>" } `
            | Select-Object -ExpandProperty InputObject `
            | Remove-Item -Force -Confirm:$false -Verbose;
    Pop-Location;

    $archiveName = "./$($ExpandedManifest["ProjectName"]).zip"

    # We make the final ZIP file using the native zip command
    # on POSIX in lieu of
    # https://github.com/PowerShell/Microsoft.PowerShell.Archive/issues/26.
    if (Test-IsPOSIX) {
        if (Get-ChildItem $archiveName -ErrorAction SilentlyContinue) {
            Remove-Item $archiveName
        }
        pushd .
        cd $tempDir
        zip -r $archiveName .
        popd
        mv (Join-Path $tempDir $archiveName) .
    } else {
        Compress-Archive -Force -Path (Join-Path $tempDir "*") -DestinationPath $archiveName
    }
    Write-Host -ForegroundColor Blue "Wrote arXiv archive to $archiveName."

    Remove-Item -Force -Recurse $tempDir;

}

## EXAMPLE MANIFEST ##

$ExampleManifest = @{
    ProjectName = "foo";
    TeXMain = "tex/foo.tex";
    AdditionalFiles = @{
        "fig/*.pdf" = $null;
        "fig/*.png" = $null;
        "tex/*.sty" = "/";
        "fig/*.mp4" = "anc/";
    };
}