Using $(SolutionDir) when running template via TextTransform.exe
Asked Answered
M

3

10

I'm trying to get our T4 templates to run at build time, without adding dependencies on the Visual Studio Modeling SDK. I've successfully used a variant of the batch file shown here, but I now have a problem in that my .tt files use the $(SolutionDir) variable to reference other projects (and thus are now not compiling).

What is the best way to handle this? What have other people done? (Hard-coding absolute paths is not an option)

EDIT: I see there's the -a argument that can be passed to TextTransform.exe, is it possible to use this to define $(SolutionDir)?

Martell answered 10/12, 2012 at 17:0 Comment(3)
Have you tried set SolutionDir=%cd% to set the variable to the current directory?Idleman
That doesn't work, neither does -a !!$(SolutionDir)!C:\dev\mysolutionrootContusion
There's a Mono port of T4. I wonder if the answer is in the codebase somewhere. See https://mcmap.net/q/35651/-t4-without-visual-studioContusion
C
4

Looking through the source code from TextTransformation.exe (with ILSpy) I don't think this is possible without modifying the template (but I do have a solution).

Ultimately what we care about here is the step during the template parsing where Microsoft.VisualStudio.TextTemplating.Engine.ResolveAssemblyReferences() is called. This delegates to ITextTemplatingEngineHost.ResolveAssemblyReference() (though it does expand environment variables first)

When the template is run from the command line, the implementation being used is that provided by the CommandLineHost, and its implementation simply looks for the file as supplied in reference paths and the GAC. Given the file name will at this point still have the $(SolutionPath) bit in, it's never going to succeed.

You could implement your own version of TextTransform.exe, but you'd have to start largely from scratch (or use reflection), since CommandLineHost is internal :-( Or you could potentially leverage the Mono port https://mcmap.net/q/35651/-t4-without-visual-studio

I can't say I'm happy about this, because I find myself in the same boat...

Edit: However... since ultimately all you need to do is change the template, I put together a PowerShell script to copy the templates to the temp directory, manually expanding the $(SolutionDir) macro in the process, and execute them from there. That seems to work just fine.

Drop this into the offending project (you might want to change the file extension) and you should be good to go:

<#
.Synopsis
Executes all the T4 templates within designated areas of the containing project

.Description
Unfortunately the Visual Studio 2010 'Transform All Templates' function doesn't appear
to work in SSDT projects, so have to resort to hackery like this to bulk-execute templates
#>
param(

)

$ErrorActionPreference = 'stop';
$scriptDir = Split-Path $MyInvocation.MyCommand.Path

$commonProgramFiles32 = $env:CommmonProgramFiles
if (Test-Path environment::"CommonProgramFiles(x86)") { $commonProgramFiles32 = (gi "Env:CommonProgramFiles(x86)").Value };

$t4 = Resolve-Path "$commonProgramFiles32\Microsoft Shared\TextTemplating\10.0\texttransform.exe";
$solutionDir = Resolve-Path "$scriptDir\..\"

$templates = @(dir "$scriptDir\Database Objects\load\*.tt")

# Cloning to temp dir originally caused issues, because I use the file name in the template (doh!)
# Now I copy to temp dir under the same name
pushd $scriptDir;
try{
    foreach($template in $templates){
        $templateTemp = Join-Path ([IO.Path]::GetTempPath()) $template.Name;
        $targetfile = [IO.Path]::ChangeExtension($template.FullName, '.sql');
        Write-Host "Running $($template.Name)"
        Write-Host "...output to $targetFile";

        # When run from outside VisualStudio you can't use $(SolutionDir)
        # ...so have to modify the template to get this to work...
        # ...do this by cloning to a temp file, and running this instead
        Get-Content $template.FullName | % {
            $_.Replace('$(SolutionDir)',"$solutionDir")
        } | Out-File -FilePath:$templateTemp

        try{
            & $t4 $templateTemp -out $targetfile -I $template.DirectoryName;
        }finally{
            if(Test-Path $templateTemp){ Remove-Item $templateTemp; }
        }
    }
}finally{
    popd;
}
Contusion answered 6/6, 2013 at 5:56 Comment(3)
Could you elaborate on this a little bit - where would one 'drop' this into the project? Would you just call this from pre- or post-build operations?Hasen
The code as written assumes the solution dir is one folder up, and finds TT files relative to itself. So just add it into the project at the root. You would execute this from the command line (in PowerShell), and yes - you could probably call this in a pre-build step if need be (I do it manually).Contusion
@Contusion Thanks for sharing. I've replaced your fixed file extension with an extension extracted from the tt file itself using regex... $extension = Select-String -Path $template.FullName -Pattern '<#@\s*output\s+extension="(.*)"\s*#>' | ForEach-Object {$_.Matches.Groups[1].Value}Sauna
I
0

I used an approach very similar to piers7 -- except in my case, I actually needed to keep the *.tt file in the same directory (because of runtime lookup of files based on relative paths), but didn't care if the file itself was named differently. As such, instead of having $templateTemp create a temporary file in a temp directory, I kept it in the same folder.

I also needed to recursively search for the *.tt files anywhere in the solution directory.

Here is the resulting script, taking piers7's and modifying it:

<#
.Synopsis
Executes all the T4 templates within designated areas of the containing project
#>
param(

)

$ErrorActionPreference = 'stop';
$scriptDir = Split-Path $MyInvocation.MyCommand.Path

$commonProgramFiles32 = $env:CommmonProgramFiles
if (Test-Path environment::"CommonProgramFiles(x86)") { $commonProgramFiles32 = (gi "Env:CommonProgramFiles(x86)").Value };

$t4 = Resolve-Path "$commonProgramFiles32\Microsoft Shared\TextTemplating\12.0\texttransform.exe";
$solutionDir = Resolve-Path "$scriptDir\"
$templates = Get-ChildItem -Path $scriptDir -Filter *.tt -Recurse
$extension = '.ts';

pushd $scriptDir;
try{
    foreach($template in $templates){
        # keeping the same path (because my template references relative paths), 
        #    but copying under different name:
        $templateTemp = $template.FullName + "____temporary"
        $targetfile = [IO.Path]::ChangeExtension($template.FullName, $extension);
        Write-Host "Running $($template.Name)"
        Write-Host "...output to $targetFile";

        # When run from outside VisualStudio you can't use $(SolutionDir)
        # ...so have to modify the template to get this to work...
        # ...do this by cloning to a temp file, and running this instead
        Get-Content $template.FullName | % {
            $_.Replace('$(SolutionDir)',"$solutionDir")
        } | Out-File -FilePath:$templateTemp

        try{
            & $t4 $templateTemp -out $targetfile -I $template.DirectoryName;
        }finally{
            if(Test-Path $templateTemp){ Remove-Item $templateTemp; }
        }
    }
}finally{
    popd;
}
Interrupter answered 22/1, 2015 at 21:16 Comment(0)
O
0

Based on piers7 code.

Powershell script Transform.ps1:

Param(
  [string]$variablesPath,
  [string]$t4File,
  [string]$targetfile
)

# Get C:\Program Files (x86)\Common Files
$commonProgramFiles32 = $env:CommmonProgramFiles
if (Test-Path environment::"CommonProgramFiles(x86)") { $commonProgramFiles32 = (gi "Env:CommonProgramFiles(x86)").Value };

# Get path t4 transformer executable
$t4 = Resolve-Path "$commonProgramFiles32\Microsoft Shared\TextTemplating\14.0\texttransform.exe";

# File object for the $t4 file (.tt)
$template = (get-item $t4File)

# Create a dictionary from the contents of system variables file
$vars = @{}
get-content $variablesPath | Foreach-Object {
    $split = $_.split("=")
    $vars.Add($split[0], $split[1])
}

# Temp file name to store the modified template.
$templateTemp = Join-Path ([IO.Path]::GetTempPath()) $template.Name;

# Read the content of the template
$content = [IO.File]::ReadAllText($template.FullName)

# Replace the variables in the template with the actual values.
($vars.Keys | Foreach-Object {
    $content = $content.Replace("`$($_)",$vars[$_])
})

# Write the modified template to the original location
$content > $templateTemp

# Execute the transformation, and delete the temporary template after done.
try{
    & $t4 $templateTemp -out $targetfile -I $template.DirectoryName;
}finally{
    if(Test-Path $templateTemp){ Remove-Item $templateTemp; }
}

# texttransform.exe seems to be messing up the BOM of the file.  Fixing.
[IO.File]::WriteAllText($targetfile, [IO.File]::ReadAllText($targetfile), [System.Text.Encoding]::UTF8)

Then from the pre-build event:

pushd $(ProjectDir)
if exist "$(ProjectDir)var.tmp.txt" del "$(ProjectDir)var.tmp.txt"
echo SolutionDir=$(SolutionDir)>>"$(ProjectDir)var.tmp.txt"
echo ProjectDir=$(ProjectDir)>>"$(ProjectDir)var.tmp.txt"
if exist "$(ProjectDir)my.cs" del "$(ProjectDir)my.cs"
Powershell.exe -ExecutionPolicy Unrestricted -File "..\..\..\Transform.ps1" "$(ProjectDir)var.tmp.txt" "$(ProjectDir)My.tt" "$(ProjectDir)My.cs"
if exist "$(ProjectDir)var.tmp.txt" del "$(ProjectDir)var.tmp.txt"
popd
Opaque answered 8/6, 2018 at 1:40 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.