Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Compute if Protobuf files need to be rebuilt #3852

Merged
merged 6 commits into from
Dec 7, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 17 additions & 20 deletions tools/IceRpc.Protobuf.Tools/IceRpc.Protobuf.Tools.targets
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@
<!-- Copyright (c) ZeroC, Inc. -->
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<!-- Import Tasks -->
<UsingTask TaskName="IceRpc.Protobuf.Tools.ProtocTask" AssemblyFile="$(IceRpcProtobufToolsTaskAssembliesPath)IceRpc.Protobuf.Tools.dll" />
<UsingTask TaskName="IceRpc.Protobuf.Tools.OutputFileNamesTask" AssemblyFile="$(IceRpcProtobufToolsTaskAssembliesPath)IceRpc.Protobuf.Tools.dll" />
<UsingTask TaskName="IceRpc.Protobuf.Tools.ProtocTask" AssemblyFile="$(IceRpcProtobufToolsTaskAssembliesPath)IceRpc.Protobuf.Tools.dll" />
<UsingTask TaskName="IceRpc.Protobuf.Tools.UpToDateCheckTask" AssemblyFile="$(IceRpcProtobufToolsTaskAssembliesPath)IceRpc.Protobuf.Tools.dll" />
<ItemGroup>
<PropertyPageSchema Include="$(MSBuildThisFileDirectory)ProtoFile.ItemDefinition.xaml" />
<AvailableItemName Include="ProtoFile" />
Expand All @@ -30,43 +31,38 @@
Exclude="$(DefaultItemExcludes);$(DefaultExcludesInProjectFolder)" />
</ItemGroup>
<Target Name="ProtoCompile" BeforeTargets="CoreCompile" Condition="@(ProtoFile) != ''">
<MakeDir Directories="%(ProtoFile.OutputDir)" />
<!-- Compile the Proto files
<MakeDir Directories="%(ProtoFile.OutputDir)" />
<UpToDateCheckTask
OutputDir="%(ProtoFile.OutputDir)"
Sources="@(ProtoFile)">
<Output
ItemName = "_ProtoFile"
TaskParameter = "ComputedSources"/>
</UpToDateCheckTask>
<!-- Compile the Proto files
The search path determines where protoc compiler locates import files, we add:
- $(IceRpcProtocPath) to import google well-known files from IceRpc.Protobuf.Tools.
- $(MSBuildProjectDirectory) to import the project own files.
- @(ProtoSearchPath) additional search paths specified by the user.
-->
<ProtocTask
WorkingDirectory="$(MSBuildProjectDirectory)"
OutputDir="%(ProtoFile.OutputDir)"
OutputDir="@(_ProtoFile->'%(OutputDir)')"
SearchPath="@(ProtoSearchPath->'%(FullPath)');$(IceRpcProtocPath);$(MSBuildProjectDirectory)"
ToolsPath="$(IceRpcProtocPath)$(IceRpcProtocPrefix)"
ScriptPath="$(IceRpcProtocGenPath)"
Sources="@(ProtoFile)">

<Output
ItemName = "_ProtoFile"
TaskParameter = "ComputedSources"/>
</ProtocTask>
Sources="%(_ProtoFile.Identity)"
Condition="'%(UpToDate)' != 'true'"/>
<!--
Include all C# generated source items that have not been already included. We delay this until we are
running the ProtoCompile target so that default includes are already processed.
-->
<ItemGroup>
<_ProtoFile>
<!--
Add GeneratePath metadata with the normalized path, this is required for excludes below to work
with different path separators.
-->
<ProtoGeneratedPath>$([MSBuild]::NormalizePath('%(OutputDir)/%(OutputFilename).cs'))</ProtoGeneratedPath>
<IceRpcGeneratedPath>$([MSBuild]::NormalizePath('%(OutputDir)/%(OutputFilename).IceRpc.cs'))</IceRpcGeneratedPath>
</_ProtoFile>
<Compile
Include="@(_ProtoFile->'%(ProtoGeneratedPath)')"
Include="@(_ProtoFile->'%(OutputDir)\%(OutputFileName).cs')"
Exclude="@(Compile->'%(FullPath)');@(Compile->'%(Identity)')" />
<Compile
Include="@(_ProtoFile->'%(IceRpcGeneratedPath)')"
Include="@(_ProtoFile->'%(OutputDir)\%(OutputFileName).IceRpc.cs')"
Exclude="@(Compile->'%(FullPath)');@(Compile->'%(Identity)')" />
</ItemGroup>
</Target>
Expand All @@ -78,6 +74,7 @@
</OutputFileNamesTask>
<Delete Files="@(_ProtoFile->'%(OutputDir)\%(OutputFilename).cs')" />
<Delete Files="@(_ProtoFile->'%(OutputDir)\%(OutputFilename).IceRpc.cs')" />
<Delete Files="@(_ProtoFile->'%(OutputDir)\%(OutputFilename).d')" />
</Target>

<!-- Package ProtoFile items -->
Expand Down
31 changes: 15 additions & 16 deletions tools/IceRpc.Protobuf.Tools/ProtocTask.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,36 +11,36 @@

namespace IceRpc.Protobuf.Tools;

/// <summary>A MSBuild task to generate C# code from Protobuf files using <c>protoc</c>.</summary>
/// <summary>A MSBuild task to generate code from Protobuf files using <c>protoc</c> C# built-in generator and
/// <c>protoc-gen-icerpc-csharp</c> generator.</summary>
public class ProtocTask : ToolTask
{
/// <summary>The output directory for the generated code; corresponds to the <c>--icerpc-csharp_out=</c> option of the
/// <c>protoc</c> compiler.</summary>
/// <summary>Gets or sets the output directory for the generated code; corresponds to the
/// <c>--icerpc-csharp_out=</c> option of the <c>protoc</c> compiler.</summary>
[Required]
public string OutputDir { get; set; } = "";

/// <summary>The directories in which to search for imports, corresponds to <c>-I</c> protoc compiler option.</summary>
/// <summary>Gets or sets the directories in which to search for imports, corresponds to <c>-I</c> protoc compiler
/// option.</summary>
public string[] SearchPath { get; set; } = Array.Empty<string>();

/// <summary>The Protobuf files to compile, these are the input files pass to the protoc compiler.</summary>
/// <summary>Gets or sets the Protobuf source files to compile, these are the input files pass to the protoc
/// compiler.</summary>
[Required]
public ITaskItem[] Sources { get; set; } = Array.Empty<ITaskItem>();

/// <summary>The directory containing the protoc compiler.</summary>
/// <summary>Gets or sets the directory containing the protoc compiler.</summary>
[Required]
public string ToolsPath { get; set; } = "";

/// <summary>The directory containing the protoc-gen-icerpc-csharp scripts.</summary>
/// <summary>Gets or sets the directory containing the protoc-gen-icerpc-csharp scripts.</summary>
[Required]
public string ScriptPath { get; set; } = "";

/// <summary>The working directory for executing the protoc compiler from.</summary>
/// <summary>Gets or sets the working directory for executing the protoc compiler from.</summary>
[Required]
public string WorkingDirectory { get; set; } = "";

[Output]
public ITaskItem[] ComputedSources { get; private set; } = Array.Empty<ITaskItem>();

/// <inheritdoc/>
protected override string ToolName =>
RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "protoc.exe" : "protoc";
Expand Down Expand Up @@ -78,12 +78,11 @@ protected override string GenerateCommandLineCommands()
searchPath.Add(directory);
}

ITaskItem computedSource = new TaskItem(source.ItemSpec);
source.CopyMetadataTo(computedSource);
computedSource.SetMetadata("OutputFileName", Path.GetFileNameWithoutExtension(fullPath).ToPascalCase());
computedSources.Add(computedSource);
// Add dependency_out to generate dependency files
builder.AppendSwitch("--dependency_out");
builder.AppendFileNameIfNotNull(
Path.Combine(OutputDir, $"{source.GetMetadata("FileName").ToPascalCase()}.d"));
}
ComputedSources = computedSources.ToArray();

// Add protoc searchPath paths
foreach (string path in searchPath)
Expand Down
104 changes: 104 additions & 0 deletions tools/IceRpc.Protobuf.Tools/UpToDateCheckTask.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
// Copyright (c) ZeroC, Inc.

using IceRpc.CaseConverter.Internal;
using Microsoft.Build.Framework;
using Microsoft.Build.Utilities;
using System;
using System.Collections.Generic;
using System.IO;

namespace IceRpc.Protobuf.Tools;

/// <summary>A MSBuild task to compute what Protobuf files have to be rebuild by <c>protoc</c>.</summary>
public class UpToDateCheckTask : Task
{
/// <summary>Gets or sets the output directory for the generated code.</summary>
[Required]
public string OutputDir { get; set; } = "";

/// <summary>Gets or sets the Protobuf source files to compute if they are up to date.</summary>
[Required]
public ITaskItem[] Sources { get; set; } = Array.Empty<ITaskItem>();

/// <summary>Gets the computed sources, which are equal to the <see cref="Sources" /> but carring additional
/// metadata.</summary>
[Output]
public ITaskItem[] ComputedSources { get; private set; } = Array.Empty<ITaskItem>();

/// <summary>Computes whether or not an output file is up to date or needs to be rebuilt. After executing this
/// task, <see cref="ComputedSources"/> contains a task item for each item in <see cref="Sources"/> with two
/// additional metadata entries. The <c>UpToDate</c> metadata is set to 'true' or 'false', indicating whether the
/// item is up to date or needs to be rebuilt. The <c>OutputFileName</c> metadata contains the base file name for
/// the generated outputs. This is the input item's file name without the extension, and converted to PascalCase.
/// </summary>
/// <returns>Returns <see langword="true"/> if the task was executed successfully, <see langword="false"/>
/// otherwise.</returns>
public override bool Execute()
{
var computedSources = new List<ITaskItem>();

foreach (ITaskItem source in Sources)
{
bool upToDate = true;
string fileName = source.GetMetadata("FileName").ToPascalCase();
string dependOutput = Path.Combine(OutputDir, $"{fileName}.d");
string csharpOutput = Path.Combine(OutputDir, $"{fileName}.cs");
string icerpcOutput = Path.Combine(OutputDir, $"{fileName}.IceRpc.cs");

if (File.Exists(dependOutput) && File.Exists(csharpOutput) && File.Exists(icerpcOutput))
{
long lastWriteTime = Math.Max(
File.GetLastWriteTime(dependOutput).Ticks,
File.GetLastWriteTime(csharpOutput).Ticks);
lastWriteTime = Math.Max(lastWriteTime, File.GetLastWriteTime(icerpcOutput).Ticks);
List<string> dependencies = ProcessDependencies(dependOutput);
foreach (string dependency in dependencies)
{
if (File.GetLastWriteTime(dependency).Ticks >= lastWriteTime)
{
// If a dependency is newer than any of the outputs the source is not up to date.
upToDate = false;
break;
}
}
}
else
{
// If any of the outputs is missing the file is not up to date.
upToDate = false;
}

ITaskItem computedSource = new TaskItem(source.ItemSpec);
source.CopyMetadataTo(computedSource);
computedSource.SetMetadata("UpToDate", upToDate ? "true" : "false");
computedSource.SetMetadata("OutputFileName", fileName);
computedSource.SetMetadata("OutputDir", OutputDir);
computedSources.Add(computedSource);
}

ComputedSources = computedSources.ToArray();
return true;

static List<string> ProcessDependencies(string dependOutput)
{
var depends = new List<string>();
string dependContents = File.ReadAllText(dependOutput);
// strip everything before Xxx.cs:
const string outputPrefix = ".cs:";
int i = dependContents.IndexOf(outputPrefix);
if (i != -1 && i + outputPrefix.Length < dependContents.Length)
{
dependContents = dependContents.Substring(i + outputPrefix.Length);
foreach (string line in dependContents.Split(new char[] { '\\' }))
{
string filePath = line.Trim();
if (!string.IsNullOrEmpty(filePath))
{
depends.Add(Path.GetFullPath(filePath));
}
}
}
return depends;
}
}
}