-
Notifications
You must be signed in to change notification settings - Fork 13
Prune stale Protobuf outputs and tighten the protoc up-to-date check #4923
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
Changes from 2 commits
0004aeb
491aa3f
c2e116d
f39b269
b35f57d
1e58b0d
7a74132
5e259e6
fa85e97
84d0d0a
e323787
ef31512
971c1e2
cb67c60
033a19c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -60,9 +60,94 @@ | |||||||||||||||||||||||
| Exclude="$(DefaultItemExcludes);$(DefaultExcludesInProjectFolder)" | ||||||||||||||||||||||||
| /> | ||||||||||||||||||||||||
| </ItemGroup> | ||||||||||||||||||||||||
| <Target Name="ProtoCompile" BeforeTargets="CoreCompile" Condition="@(ProtoFile) != ''"> | ||||||||||||||||||||||||
| <!-- | ||||||||||||||||||||||||
| Computes the files that protoc produces for the current ProtoFile items, as absolute paths, and the fingerprint | ||||||||||||||||||||||||
| of the configuration that produces them. ProtoCompile and the prune, manifest and clean targets share it, and | ||||||||||||||||||||||||
| MSBuild runs it once per build. | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| It deliberately doesn't run the up-to-date check, and ProtoCompile keeps its own _ProtoFile item: Rebuild runs | ||||||||||||||||||||||||
| Clean and Build in the same project instance, so an up-to-date check made before Clean would mark as current the | ||||||||||||||||||||||||
| very outputs Clean is about to delete. A task <Output> also appends to an existing item, so sharing _ProtoFile | ||||||||||||||||||||||||
| with ProtoCompile would leave it with two entries per ProtoFile. | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| The task assembly only exists in source builds once IceRpc.Protobuf.Tools has been built, which isn't the case | ||||||||||||||||||||||||
| for a Clean that precedes any Build; the Exists check skips the computation in that case. | ||||||||||||||||||||||||
| --> | ||||||||||||||||||||||||
| <Target Name="_ComputeProtoOutputs" | ||||||||||||||||||||||||
| Condition="@(ProtoFile) != '' And Exists('$(IceRpcProtobufToolsTaskAssembliesPath)IceRpc.Protobuf.Tools.dll')"> | ||||||||||||||||||||||||
| <OutputFileNamesTask Sources="@(ProtoFile)"> | ||||||||||||||||||||||||
| <Output ItemName="_ProtoNamedFile" TaskParameter="ComputedSources" /> | ||||||||||||||||||||||||
| </OutputFileNamesTask> | ||||||||||||||||||||||||
| <ItemGroup> | ||||||||||||||||||||||||
| <_ProtoOutput Include="$([MSBuild]::NormalizePath('%(_ProtoNamedFile.OutputDir)/%(_ProtoNamedFile.OutputFileName).cs'))" /> | ||||||||||||||||||||||||
| <_ProtoOutput Include="$([MSBuild]::NormalizePath('%(_ProtoNamedFile.OutputDir)/%(_ProtoNamedFile.OutputFileName).IceRpc.cs'))" /> | ||||||||||||||||||||||||
| <_ProtoOutput Include="$([MSBuild]::NormalizePath('%(_ProtoNamedFile.OutputDir)/%(_ProtoNamedFile.OutputFileName).d'))" /> | ||||||||||||||||||||||||
| <!-- The xxx.BuildTelemetry.txt is generated when both IceRpcBuildTelemetry and IceRpcBuildTelemetryDebug are true --> | ||||||||||||||||||||||||
| <_ProtoOutput | ||||||||||||||||||||||||
| Include="$([MSBuild]::NormalizePath('%(_ProtoNamedFile.OutputDir)/%(_ProtoNamedFile.OutputFileName).BuildTelemetry.txt'))" | ||||||||||||||||||||||||
| Condition="'$(IceRpcBuildTelemetry)' == 'true' And '$(IceRpcBuildTelemetryDebug)' == 'true'" /> | ||||||||||||||||||||||||
| </ItemGroup> | ||||||||||||||||||||||||
| <PropertyGroup> | ||||||||||||||||||||||||
| <!-- | ||||||||||||||||||||||||
| Everything that changes the generated code without touching a Proto file: the compiler and generator | ||||||||||||||||||||||||
| versions, the import search path and the per-file protoc options. A change regenerates every Proto file. | ||||||||||||||||||||||||
| --> | ||||||||||||||||||||||||
| <_ProtocFingerprint>protoc=$(ProtocBundledPluginVersion);icerpc-csharp=$(ProtocIceRpcPluginVersion);search-path=@(ProtoSearchPath->'%(FullPath)', ',');options=@(ProtoFile->'%(Identity)=%(AdditionalOptions)', ',')</_ProtocFingerprint> | ||||||||||||||||||||||||
| </PropertyGroup> | ||||||||||||||||||||||||
| </Target> | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| <!-- | ||||||||||||||||||||||||
| Delete generated files recorded in the previous build's manifest that the current build no longer produces. | ||||||||||||||||||||||||
| Without this, a removed or renamed .proto file (or a ProtoFile whose OutputDir changed) leaves orphan .cs files | ||||||||||||||||||||||||
| behind that the SDK's default Compile glob keeps picking up. | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| BeforeTargets covers both the ProtoFile-present case (ProtoCompile/ProtoClean) and the ProtoFile-empty case where | ||||||||||||||||||||||||
| those targets are skipped by their conditions but stale files from a previous build still need pruning | ||||||||||||||||||||||||
| (CoreCompile/Clean). | ||||||||||||||||||||||||
| --> | ||||||||||||||||||||||||
| <Target Name="_ProtoPruneStaleOutputs" | ||||||||||||||||||||||||
| BeforeTargets="ProtoCompile;CoreCompile;ProtoClean;Clean" | ||||||||||||||||||||||||
| DependsOnTargets="_ComputeProtoOutputs" | ||||||||||||||||||||||||
| Condition="Exists('$(IntermediateOutputPath)protoc.outputs.txt')"> | ||||||||||||||||||||||||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🤖 Claude: Reproduced: a Debug build, a removed or renamed Proto file, then a Release build compiles the stale output until Debug is built again. The per-configuration/TargetFramework manifest deliberately mirrors IceRpc.Slice.Tools (#4589), so parallel multi-target inner builds do not share one state file. Building several configurations against the same shared
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. When
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🤖 Claude: Applied in e323787. Verified: with the task assembly path pointing at a missing directory and |
||||||||||||||||||||||||
| <ReadLinesFromFile File="$(IntermediateOutputPath)protoc.outputs.txt"> | ||||||||||||||||||||||||
| <Output TaskParameter="Lines" ItemName="_PreviousProtoOutput" /> | ||||||||||||||||||||||||
| </ReadLinesFromFile> | ||||||||||||||||||||||||
| <ItemGroup> | ||||||||||||||||||||||||
| <_StaleProtoOutput Include="@(_PreviousProtoOutput)" Exclude="@(_ProtoOutput)" /> | ||||||||||||||||||||||||
| <!-- | ||||||||||||||||||||||||
| The SDK's default Compile glob captured these paths during item evaluation, with project-relative | ||||||||||||||||||||||||
| Identities. We materialize the relative form into its own item so that <Compile Remove> can match it | ||||||||||||||||||||||||
| - inlining the MakeRelative call inside an item transform pattern doesn't evaluate the property | ||||||||||||||||||||||||
| function as expected. | ||||||||||||||||||||||||
| --> | ||||||||||||||||||||||||
| <_StaleProtoOutputRelative | ||||||||||||||||||||||||
| Include="$([MSBuild]::MakeRelative($(MSBuildProjectDirectory), %(_StaleProtoOutput.Identity)))" | ||||||||||||||||||||||||
| Condition="'%(_StaleProtoOutput.Identity)' != ''" /> | ||||||||||||||||||||||||
| <!-- | ||||||||||||||||||||||||
| Remove now, otherwise csc still reports the stale files as inputs and fails with CS2001 once we | ||||||||||||||||||||||||
| delete them on disk. We try both the absolute and relative form to cover Compile entries added by | ||||||||||||||||||||||||
| either explicit ProjectReference users or the SDK glob. | ||||||||||||||||||||||||
| --> | ||||||||||||||||||||||||
| <Compile Remove="@(_StaleProtoOutput)" /> | ||||||||||||||||||||||||
| <Compile Remove="@(_StaleProtoOutputRelative)" /> | ||||||||||||||||||||||||
| </ItemGroup> | ||||||||||||||||||||||||
| <Delete Files="@(_StaleProtoOutput)" /> | ||||||||||||||||||||||||
| </Target> | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| <Target Name="ProtoCompile" BeforeTargets="CoreCompile" DependsOnTargets="_ComputeProtoOutputs" Condition="@(ProtoFile) != ''"> | ||||||||||||||||||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 129 columns.
Suggested change
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🤖 Claude: Done in e323787. |
||||||||||||||||||||||||
| <ItemGroup> | ||||||||||||||||||||||||
| <!-- Tools whose update must regenerate the code, in addition to the inputs recorded in the dependency files. --> | ||||||||||||||||||||||||
| <_ProtocInput Include="$(IceRpcProtocPath)$(IceRpcProtocPrefix)/protoc$(_ProtocExecutableExtension)" /> | ||||||||||||||||||||||||
| <_ProtocInput Include="$(IceRpcProtocGenPath)IceRpc.Protobuf.Generator.dll" /> | ||||||||||||||||||||||||
| </ItemGroup> | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| <MakeDir Directories="%(ProtoFile.OutputDir)" /> | ||||||||||||||||||||||||
| <UpToDateCheckTask OutputDir="%(ProtoFile.OutputDir)" Sources="@(ProtoFile)"> | ||||||||||||||||||||||||
| <UpToDateCheckTask | ||||||||||||||||||||||||
| OutputDir="%(ProtoFile.OutputDir)" | ||||||||||||||||||||||||
| Sources="@(ProtoFile)" | ||||||||||||||||||||||||
| AdditionalInputs="@(_ProtocInput)" | ||||||||||||||||||||||||
| Fingerprint="$(_ProtocFingerprint)" | ||||||||||||||||||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. See the review body: this parameter,
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🤖 Claude: Done in e323787: |
||||||||||||||||||||||||
| FingerprintFile="$(IntermediateOutputPath)protoc.fingerprint.txt"> | ||||||||||||||||||||||||
|
pepone marked this conversation as resolved.
Outdated
|
||||||||||||||||||||||||
| <Output ItemName="_ProtoFile" TaskParameter="ComputedSources" /> | ||||||||||||||||||||||||
| </UpToDateCheckTask> | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
|
|
@@ -121,14 +206,36 @@ | |||||||||||||||||||||||
| /> | ||||||||||||||||||||||||
| </ItemGroup> | ||||||||||||||||||||||||
| </Target> | ||||||||||||||||||||||||
| <Target Name="ProtoClean" BeforeTargets="Clean" Condition="Exists('$(IceRpcProtobufToolsTaskAssembliesPath)IceRpc.Protobuf.Tools.dll')"> | ||||||||||||||||||||||||
| <OutputFileNamesTask Sources="@(ProtoFile)"> | ||||||||||||||||||||||||
| <Output ItemName="_ProtoFile" TaskParameter="ComputedSources" /> | ||||||||||||||||||||||||
| </OutputFileNamesTask> | ||||||||||||||||||||||||
| <Delete Files="@(_ProtoFile->'%(OutputDir)/%(OutputFilename).cs')" /> | ||||||||||||||||||||||||
| <Delete Files="@(_ProtoFile->'%(OutputDir)/%(OutputFilename).IceRpc.cs')" /> | ||||||||||||||||||||||||
| <Delete Files="@(_ProtoFile->'%(OutputDir)/%(OutputFilename).d')" /> | ||||||||||||||||||||||||
| <Delete Files="@(_ProtoFile->'%(OutputDir)/%(OutputFilename).BuildTelemetry.txt')" /> | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| <!-- | ||||||||||||||||||||||||
| Record this build's outputs and configuration so the next build can prune anything no longer produced and | ||||||||||||||||||||||||
| detect a configuration change. As an AfterTargets of ProtoCompile this only runs once protoc succeeded, so a | ||||||||||||||||||||||||
| failed run never records a fingerprint its outputs don't match. The fingerprint is escaped because | ||||||||||||||||||||||||
| WriteLinesToFile would otherwise split it on the semicolons. | ||||||||||||||||||||||||
| --> | ||||||||||||||||||||||||
| <Target Name="_ProtoWriteOutputManifest" | ||||||||||||||||||||||||
| AfterTargets="ProtoCompile" | ||||||||||||||||||||||||
| DependsOnTargets="_ComputeProtoOutputs" | ||||||||||||||||||||||||
| Condition="@(ProtoFile) != ''"> | ||||||||||||||||||||||||
| <MakeDir Directories="$(IntermediateOutputPath)" /> | ||||||||||||||||||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. WriteLinesToFile creates the directory, and PrepareForBuild already created it. Drop the line.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🤖 Claude: Done in e323787. |
||||||||||||||||||||||||
| <WriteLinesToFile | ||||||||||||||||||||||||
| File="$(IntermediateOutputPath)protoc.outputs.txt" | ||||||||||||||||||||||||
| Lines="@(_ProtoOutput)" | ||||||||||||||||||||||||
| Overwrite="true" | ||||||||||||||||||||||||
| WriteOnlyWhenDifferent="true" /> | ||||||||||||||||||||||||
| <WriteLinesToFile | ||||||||||||||||||||||||
| File="$(IntermediateOutputPath)protoc.fingerprint.txt" | ||||||||||||||||||||||||
| Lines="$([MSBuild]::Escape($(_ProtocFingerprint)))" | ||||||||||||||||||||||||
| Overwrite="true" | ||||||||||||||||||||||||
| WriteOnlyWhenDifferent="true" /> | ||||||||||||||||||||||||
| </Target> | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| <Target Name="ProtoClean" BeforeTargets="Clean" DependsOnTargets="_ComputeProtoOutputs"> | ||||||||||||||||||||||||
| <Delete Files="@(_ProtoOutput)" /> | ||||||||||||||||||||||||
| <Delete Files="$(IntermediateOutputPath)protoc.outputs.txt" | ||||||||||||||||||||||||
| Condition="Exists('$(IntermediateOutputPath)protoc.outputs.txt')" /> | ||||||||||||||||||||||||
| <Delete Files="$(IntermediateOutputPath)protoc.fingerprint.txt" | ||||||||||||||||||||||||
| Condition="Exists('$(IntermediateOutputPath)protoc.fingerprint.txt')" /> | ||||||||||||||||||||||||
| </Target> | ||||||||||||||||||||||||
|
Comment on lines
+227
to
232
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Delete skips missing files (the first Delete already relies on that), so the Exists guards are redundant and one task does it. Same for the Exists half of the condition on the Delete in ProtoCompile, moot if the fingerprint becomes a cache file.
Suggested change
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🤖 Claude: Done in e323787; the Delete now covers |
||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| <!-- Package ProtoFile items --> | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -75,9 +75,15 @@ once per Proto file. | |||||||||||||||||
| | Pack | `false` | Specifies whether or not to include the items (Proto files) in the NuGet package. | | ||||||||||||||||||
| | PackagePath | protobuf | Sets the target path in the NuGet package. Used only when Pack is `true`. | | ||||||||||||||||||
|
|
||||||||||||||||||
| > [!NOTE] | ||||||||||||||||||
| > Changing `AdditionalOptions` does not mark previously generated code as out of date. Run `dotnet clean` and then | ||||||||||||||||||
| > build again to regenerate the code with the new options. | ||||||||||||||||||
| ## Incremental builds | ||||||||||||||||||
|
|
||||||||||||||||||
| `protoc` runs only for the Proto files whose generated code is missing or out of date. A Proto file is out of date | ||||||||||||||||||
| when the Proto file itself, one of the files it imports, `protoc` or the `protoc-gen-icerpc-csharp` generator is newer | ||||||||||||||||||
| than one of its generated files. Changing `AdditionalOptions` or `ProtoSearchPath`, or upgrading this package, | ||||||||||||||||||
| regenerates the code of all Proto files. | ||||||||||||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. protoc doesn't run on its own; the build runs it.
Suggested change
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🤖 Claude: Done in e323787. |
||||||||||||||||||
|
|
||||||||||||||||||
| The generated code of a Proto file that is removed from the project, renamed, or given a different `OutputDir` is | ||||||||||||||||||
| deleted during the next build. | ||||||||||||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Three stacked qualifiers on a passive sentence; active voice reads better.
Suggested change
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🤖 Claude: Done in e323787. |
||||||||||||||||||
|
|
||||||||||||||||||
| ## Generated code and NuGet packages | ||||||||||||||||||
|
|
||||||||||||||||||
|
|
||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
options=@(ProtoFile->'%(Identity)=%(AdditionalOptions)', ',')changes whenever a Proto file is added, removed or renamed, so each of those regenerates every Proto file in the project. On main, adding a file ran protoc once. The README's "Incremental builds" section describes the per-file behavior, not this.Only the option values need to be in the fingerprint, not the file set: for example the distinct
%(AdditionalOptions)values, or a per-file record next to the.dcompared in the task.Verified at this head: adding a Proto file to IceRpc.Protobuf.Tests logs "The protoc configuration changed since the previous build; all Proto files are out of date".
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🤖 Claude: Done in e323787. The cache records the distinct
AdditionalOptionsvalues (@(ProtoFile->'options=%(AdditionalOptions)'->DistinctWithCase()); with-case becauseDistinct()folds case and protoc options don't). Verified on the branch: adding a Proto file runs protoc once, removing it runs nothing and prunes its outputs. One limitation of the distinct set, for the record: a file switching to an option value another file already uses leaves the set unchanged and isn't regenerated. A per-file record next to the.dwould close that at the cost of a manifest entry, clean/prune participation and a compare in the task; say if you'd rather have that.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes to the per-file record. protoc runs once per Proto file, so the options are a per-file input, and the build
already keeps per-file state for every Proto file: its dependency file. Record the options next to it and the check
becomes exact instead of a set approximation. The cache file stays for the inputs that are global: the versions and
the search path.
It is smaller than it sounds:
_ComputeProtoOutputsgets one more_ProtoOutputInclude,%(OutputFileName).options(or a name you prefer).That one line covers the manifest, prune and clean.
ProtoCompilegets aWriteLinesToFileafterProtocTask, batched on%(_ProtoFile...)with the sameUpToDate != 'true'condition,Lines="%(_ProtoFile.AdditionalOptions)". Running after protoc, a failed runleaves no fresh record.
UpToDateCheckTaskadds the record tooutputsand compares its lines with theAdditionalOptionsmetadata italready gets on each source. Missing or different means out of date. No new parameter.
The
options=line, theDistinctWithCasetransform and its comment then leave the cache.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🤖 Claude: Done in 033a19c, with one change to the mechanism. The record is
<OutputDir>/<Name>.options, one option per line, listed by_ComputeProtoOutputsso the manifest, prune and clean cover it, and theoptions=line andDistinctWithCaseleft the cache. ButProtoCompilewrites it before the check withWriteOnlyWhenDifferent, and the task lists it among the file's inputs, the same rule asprotoc.inputs.cacheapplied per file. Two reasons, both reproduced with the after-protoc version first:%3Band, on Unix, for a backslash in an option value. With the record as an input there is nothing to compare.Verified with a two-file probe: one file's options changed / unchanged / reverted regenerates that file only (1 / 0 / 1); a file switching to the value the other uses, 1; a sibling failure with an options change regenerates only the failed file once fixed (1), and with the change reverted, the reverted file as well (2); third file added 1, removed 0 with
Third.optionspruned; task assembly missing, task-load error with the generated code intact; Clean removes the records.IceRpc.Protobuf.Tests: first build 5 (no records yet), second 0, 70 tests pass.