From 773ee9f19ead58d35d0e2e03d2432c7b6611fc82 Mon Sep 17 00:00:00 2001 From: Aditya Singh Date: Fri, 5 Jun 2026 04:21:52 -0700 Subject: [PATCH 1/4] Always use PathToUnderscores naming for SwiftPM plugin outputs The SwiftProtobufPlugin build tool declared each generated file using the input proto's full relative path with the suffix swapped to .pb.swift, while passing the configured FileNaming straight through to protoc-gen-swift. When a target used PathToUnderscores or DropPath, or when two protos in different subdirectories shared a file name, the generator wrote a differently named file than the plugin declared and the build failed because the declared output did not exist. This is the problem reported in issue 1761. The generated files live in the build directory where their names are never observed, so there is no value in offering a file naming choice here. The plugin now always generates with PathToUnderscores and derives the declared output names with the same transformation, replacing the directory separators with underscores. Pinning both the generator invocation and the declared outputs to one strategy means they can never diverge, which also removes the need to mirror the generator's FileGenerator.outputFilename logic. The fileNaming config field already existed on main, so it is kept for backwards compatibility. It may be omitted or set to PathToUnderscores. Any other value is now a clear configuration error rather than a silently ignored setting. A PathToUnderscores example target builds two protos that share the file name Duplicate.proto in different subdirectories, which only succeeds because of the underscore naming, covering the case from issue 1761. Fixes #1761 --- PluginExamples/Package.swift | 10 +++++ PluginExamples/Package@swift-6.1.swift | 10 +++++ .../Sources/ExampleTests/ExampleTests.swift | 12 +++++ .../Proto/bar/Duplicate.proto | 5 +++ .../Proto/foo/Duplicate.proto | 5 +++ .../Sources/PathToUnderscores/empty.swift | 3 ++ .../swift-protobuf-config.json | 11 +++++ Plugins/SwiftProtobufPlugin/plugin.swift | 45 ++++++++++++++----- 8 files changed, 90 insertions(+), 11 deletions(-) create mode 100644 PluginExamples/Sources/PathToUnderscores/Proto/bar/Duplicate.proto create mode 100644 PluginExamples/Sources/PathToUnderscores/Proto/foo/Duplicate.proto create mode 100644 PluginExamples/Sources/PathToUnderscores/empty.swift create mode 100644 PluginExamples/Sources/PathToUnderscores/swift-protobuf-config.json diff --git a/PluginExamples/Package.swift b/PluginExamples/Package.swift index 46e7bc6ac..82327bbbf 100644 --- a/PluginExamples/Package.swift +++ b/PluginExamples/Package.swift @@ -66,6 +66,15 @@ let package = Package( .plugin(name: "SwiftProtobufPlugin", package: "swift-protobuf") ] ), + .target( + name: "PathToUnderscores", + dependencies: [ + .product(name: "SwiftProtobuf", package: "swift-protobuf") + ], + plugins: [ + .plugin(name: "SwiftProtobufPlugin", package: "swift-protobuf") + ] + ), .testTarget( name: "ExampleTests", dependencies: [ @@ -76,6 +85,7 @@ let package = Package( .target(name: "CustomProtoPath"), .product(name: "Nonexhaustive", package: "Subpackage"), .target(name: "UsesWKTs"), + .target(name: "PathToUnderscores"), ] ), ], diff --git a/PluginExamples/Package@swift-6.1.swift b/PluginExamples/Package@swift-6.1.swift index 92d4b26e5..9693f6eb5 100644 --- a/PluginExamples/Package@swift-6.1.swift +++ b/PluginExamples/Package@swift-6.1.swift @@ -65,6 +65,15 @@ let package = Package( .plugin(name: "SwiftProtobufPlugin", package: "swift-protobuf") ] ), + .target( + name: "PathToUnderscores", + dependencies: [ + .product(name: "SwiftProtobuf", package: "swift-protobuf") + ], + plugins: [ + .plugin(name: "SwiftProtobufPlugin", package: "swift-protobuf") + ] + ), .testTarget( name: "ExampleTests", dependencies: [ @@ -74,6 +83,7 @@ let package = Package( .target(name: "AccessLevelOnImport"), .target(name: "CustomProtoPath"), .target(name: "UsesWKTs"), + .target(name: "PathToUnderscores"), ] ), ], diff --git a/PluginExamples/Sources/ExampleTests/ExampleTests.swift b/PluginExamples/Sources/ExampleTests/ExampleTests.swift index 257197989..341f1304c 100644 --- a/PluginExamples/Sources/ExampleTests/ExampleTests.swift +++ b/PluginExamples/Sources/ExampleTests/ExampleTests.swift @@ -1,6 +1,7 @@ import CustomProtoPath import Import import Nested +import PathToUnderscores import Simple import UsesWKTs import XCTest @@ -61,4 +62,15 @@ final class ExampleTests: XCTestCase { XCTAssertEqual(usesWKTs.name, "UsesWKTs") XCTAssertEqual(usesWKTs.aTimestamp.seconds, 2) } + + // The two protos in this target share the file name Duplicate.proto in different + // subdirectories. The build only succeeds because the plugin generates them with + // PathToUnderscores naming, so they land in distinctly named files instead of + // colliding. This is the case reported in issue 1761. + func testPathToUnderscores() { + let foo = FooDuplicate.with { $0.name = "Foo" } + let bar = BarDuplicate.with { $0.name = "Bar" } + XCTAssertEqual(foo.name, "Foo") + XCTAssertEqual(bar.name, "Bar") + } } diff --git a/PluginExamples/Sources/PathToUnderscores/Proto/bar/Duplicate.proto b/PluginExamples/Sources/PathToUnderscores/Proto/bar/Duplicate.proto new file mode 100644 index 000000000..1c9cf72f4 --- /dev/null +++ b/PluginExamples/Sources/PathToUnderscores/Proto/bar/Duplicate.proto @@ -0,0 +1,5 @@ +syntax = "proto3"; + +message BarDuplicate { + string name = 1; +} diff --git a/PluginExamples/Sources/PathToUnderscores/Proto/foo/Duplicate.proto b/PluginExamples/Sources/PathToUnderscores/Proto/foo/Duplicate.proto new file mode 100644 index 000000000..105f664aa --- /dev/null +++ b/PluginExamples/Sources/PathToUnderscores/Proto/foo/Duplicate.proto @@ -0,0 +1,5 @@ +syntax = "proto3"; + +message FooDuplicate { + string name = 1; +} diff --git a/PluginExamples/Sources/PathToUnderscores/empty.swift b/PluginExamples/Sources/PathToUnderscores/empty.swift new file mode 100644 index 000000000..d4445d2a5 --- /dev/null +++ b/PluginExamples/Sources/PathToUnderscores/empty.swift @@ -0,0 +1,3 @@ +/// DO NOT DELETE. +/// +/// We need to keep this file otherwise the plugin is not running. diff --git a/PluginExamples/Sources/PathToUnderscores/swift-protobuf-config.json b/PluginExamples/Sources/PathToUnderscores/swift-protobuf-config.json new file mode 100644 index 000000000..4a6fc432e --- /dev/null +++ b/PluginExamples/Sources/PathToUnderscores/swift-protobuf-config.json @@ -0,0 +1,11 @@ +{ + "invocations": [ + { + "protoFiles": [ + "Proto/foo/Duplicate.proto", + "Proto/bar/Duplicate.proto", + ], + "visibility": "public" + } + ] +} diff --git a/Plugins/SwiftProtobufPlugin/plugin.swift b/Plugins/SwiftProtobufPlugin/plugin.swift index b0e5a8484..38587c659 100644 --- a/Plugins/SwiftProtobufPlugin/plugin.swift +++ b/Plugins/SwiftProtobufPlugin/plugin.swift @@ -11,6 +11,8 @@ struct SwiftProtobufPlugin { case invalidInputFileExtension(String) /// Indicates that there was no configuration file at the required location. case noConfigFound(String) + /// Indicates that the configuration set a `fileNaming` option the build plugin does not support. + case unsupportedFileNaming(String) var description: String { switch self { @@ -23,6 +25,13 @@ struct SwiftProtobufPlugin { No configuration file found named '\(path)'. The file must not be listed in the \ 'exclude:' argument for the target in Package.swift. """ + case let .unsupportedFileNaming(value): + return """ + The 'fileNaming' option '\(value)' is not supported by the build plugin. The build \ + plugin always generates files using the 'PathToUnderscores' naming because the \ + generated files go into the build directory and the name is never observed. Remove \ + the 'fileNaming' option or set it to 'PathToUnderscores'. + """ } } } @@ -106,6 +115,12 @@ struct SwiftProtobufPlugin { /// The visibility of the generated files. var visibility: Visibility? /// The file naming strategy to use. + /// + /// The build plugin always generates files with `PathToUnderscores` naming. The + /// generated files live in the build directory, so the file name on disk is never + /// observed and there is no benefit to giving users a choice here. This field is kept + /// for backwards compatibility: it may be omitted or set to `PathToUnderscores`. Any + /// other value is rejected so an explicit setting is never silently ignored. var fileNaming: FileNaming? /// Whether internal imports should be annotated as `@_implementationOnly`. var implementationOnlyImports: Bool? @@ -222,10 +237,11 @@ struct SwiftProtobufPlugin { protocArgs.append("--swift_opt=Visibility=\(visibility.rawValue)") } - // Add the file naming if it was set - if let fileNaming = invocation.fileNaming { - protocArgs.append("--swift_opt=FileNaming=\(fileNaming.rawValue)") - } + // Always generate with PathToUnderscores naming. The declared output paths below are + // derived the same way, so the names the build system expects can never drift from the + // names protoc-gen-swift actually writes. The configured fileNaming, if any, was already + // validated to be PathToUnderscores. + protocArgs.append("--swift_opt=FileNaming=\(Configuration.Invocation.FileNaming.pathToUnderscores.rawValue)") // Add the implementation only imports flag if it was set if let implementationOnlyImports = invocation.implementationOnlyImports { @@ -244,17 +260,19 @@ struct SwiftProtobufPlugin { var inputFiles = [URL]() var outputFiles = [URL]() - for var file in invocation.protoFiles { + for file in invocation.protoFiles { // Append the file to the protoc args so that it is used for generating protocArgs.append(file) inputFiles.append(protoDirectory.appending(path: file)) - // The name of the output file is based on the name of the input file. - // We validated in the beginning that every file has the suffix of .proto - // This means we can just drop the last 5 elements and append the new suffix - file.removeLast(5) - file.append("pb.swift") - let protobufOutputPath = outputDirectory.appending(path: file) + // The output file name has to match exactly what protoc-gen-swift writes, otherwise + // the build system looks for a file that does not exist and the build fails. We always + // generate with PathToUnderscores naming, so the relative proto path becomes a single + // file name with the directory separators replaced by underscores. We validated up + // front that every file has the .proto suffix, which is dropped for .pb.swift. + let outputName = String(file.dropLast(".proto".count)) + .replacingOccurrences(of: "/", with: "_") + ".pb.swift" + let protobufOutputPath = outputDirectory.appending(path: outputName) // Add the outputPath as an output file outputFiles.append(protobufOutputPath) @@ -280,6 +298,11 @@ struct SwiftProtobufPlugin { throw PluginError.invalidInputFileExtension(protoFile) } } + // The build plugin always uses PathToUnderscores naming. Reject any other explicit + // value so the setting is never silently ignored. + if let fileNaming = invocation.fileNaming, fileNaming != .pathToUnderscores { + throw PluginError.unsupportedFileNaming(fileNaming.rawValue) + } } } } From b2cdeaaa8dc5ce96f21052c1d2d95bdd04eb5356 Mon Sep 17 00:00:00 2001 From: Aditya Singh Date: Fri, 5 Jun 2026 07:33:36 -0700 Subject: [PATCH 2/4] Hardcode the FileNaming protoc option string in the plugin --- Plugins/SwiftProtobufPlugin/plugin.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Plugins/SwiftProtobufPlugin/plugin.swift b/Plugins/SwiftProtobufPlugin/plugin.swift index 38587c659..df11dff4f 100644 --- a/Plugins/SwiftProtobufPlugin/plugin.swift +++ b/Plugins/SwiftProtobufPlugin/plugin.swift @@ -241,7 +241,7 @@ struct SwiftProtobufPlugin { // derived the same way, so the names the build system expects can never drift from the // names protoc-gen-swift actually writes. The configured fileNaming, if any, was already // validated to be PathToUnderscores. - protocArgs.append("--swift_opt=FileNaming=\(Configuration.Invocation.FileNaming.pathToUnderscores.rawValue)") + protocArgs.append("--swift_opt=FileNaming=PathToUnderscores") // Add the implementation only imports flag if it was set if let implementationOnlyImports = invocation.implementationOnlyImports { From 5351ec3f7c7bd01e96f9e3a0315f2ebf75dbac3b Mon Sep 17 00:00:00 2001 From: Aditya Singh Date: Fri, 5 Jun 2026 12:43:15 -0700 Subject: [PATCH 3/4] Warn instead of error when fileNaming is set to an ignored value --- Plugins/SwiftProtobufPlugin/plugin.swift | 21 +++++++++------------ 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/Plugins/SwiftProtobufPlugin/plugin.swift b/Plugins/SwiftProtobufPlugin/plugin.swift index df11dff4f..958411d4c 100644 --- a/Plugins/SwiftProtobufPlugin/plugin.swift +++ b/Plugins/SwiftProtobufPlugin/plugin.swift @@ -11,8 +11,6 @@ struct SwiftProtobufPlugin { case invalidInputFileExtension(String) /// Indicates that there was no configuration file at the required location. case noConfigFound(String) - /// Indicates that the configuration set a `fileNaming` option the build plugin does not support. - case unsupportedFileNaming(String) var description: String { switch self { @@ -25,13 +23,6 @@ struct SwiftProtobufPlugin { No configuration file found named '\(path)'. The file must not be listed in the \ 'exclude:' argument for the target in Package.swift. """ - case let .unsupportedFileNaming(value): - return """ - The 'fileNaming' option '\(value)' is not supported by the build plugin. The build \ - plugin always generates files using the 'PathToUnderscores' naming because the \ - generated files go into the build directory and the name is never observed. Remove \ - the 'fileNaming' option or set it to 'PathToUnderscores'. - """ } } } @@ -298,10 +289,16 @@ struct SwiftProtobufPlugin { throw PluginError.invalidInputFileExtension(protoFile) } } - // The build plugin always uses PathToUnderscores naming. Reject any other explicit - // value so the setting is never silently ignored. + // The build plugin always uses PathToUnderscores naming. Warn on any other explicit + // value so the setting is never silently ignored, without breaking existing builds. if let fileNaming = invocation.fileNaming, fileNaming != .pathToUnderscores { - throw PluginError.unsupportedFileNaming(fileNaming.rawValue) + Diagnostics.warning( + """ + The 'fileNaming' option '\(fileNaming.rawValue)' is ignored by the build plugin. The build \ + plugin always generates files using the 'PathToUnderscores' naming because the \ + generated files go into the build directory and the name is never observed. + """ + ) } } } From 620ff4bcd01051a54e0c8f2080e981fc04405a56 Mon Sep 17 00:00:00 2001 From: Aditya Singh Date: Thu, 11 Jun 2026 09:44:03 -0700 Subject: [PATCH 4/4] Move fileNaming warning out of the per-file validation loop Emit the ignored-fileNaming diagnostic once per invocation in invokeProtoc, just before the FileNaming=PathToUnderscores arg is added, instead of in the validateConfiguration loop. The behavior is unchanged, the warning just no longer risks being repeated per file. --- Plugins/SwiftProtobufPlugin/plugin.swift | 28 +++++++++++++----------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/Plugins/SwiftProtobufPlugin/plugin.swift b/Plugins/SwiftProtobufPlugin/plugin.swift index 958411d4c..fc1f3c273 100644 --- a/Plugins/SwiftProtobufPlugin/plugin.swift +++ b/Plugins/SwiftProtobufPlugin/plugin.swift @@ -228,10 +228,23 @@ struct SwiftProtobufPlugin { protocArgs.append("--swift_opt=Visibility=\(visibility.rawValue)") } + // The build plugin always uses PathToUnderscores naming. Warn once per invocation on any + // other explicit value so the setting is never silently ignored, without breaking existing + // builds. + if let fileNaming = invocation.fileNaming, fileNaming != .pathToUnderscores { + Diagnostics.warning( + """ + The 'fileNaming' option '\(fileNaming.rawValue)' is ignored by the build plugin. The build \ + plugin always generates files using the 'PathToUnderscores' naming because the \ + generated files go into the build directory and the name is never observed. + """ + ) + } + // Always generate with PathToUnderscores naming. The declared output paths below are // derived the same way, so the names the build system expects can never drift from the - // names protoc-gen-swift actually writes. The configured fileNaming, if any, was already - // validated to be PathToUnderscores. + // names protoc-gen-swift actually writes. The configured fileNaming, if any, only triggers + // the warning above and does not change the naming used. protocArgs.append("--swift_opt=FileNaming=PathToUnderscores") // Add the implementation only imports flag if it was set @@ -289,17 +302,6 @@ struct SwiftProtobufPlugin { throw PluginError.invalidInputFileExtension(protoFile) } } - // The build plugin always uses PathToUnderscores naming. Warn on any other explicit - // value so the setting is never silently ignored, without breaking existing builds. - if let fileNaming = invocation.fileNaming, fileNaming != .pathToUnderscores { - Diagnostics.warning( - """ - The 'fileNaming' option '\(fileNaming.rawValue)' is ignored by the build plugin. The build \ - plugin always generates files using the 'PathToUnderscores' naming because the \ - generated files go into the build directory and the name is never observed. - """ - ) - } } } }