1 // Protocol Buffers - Google's data interchange format
2 // Copyright 2008 Google Inc. All rights reserved.
3 //
4 // Use of this source code is governed by a BSD-style
5 // license that can be found in the LICENSE file or at
6 // https://developers.google.com/open-source/licenses/bsd
7
8 #include "google/protobuf/compiler/objectivec/generator.h"
9
10 #include <cstddef>
11 #include <cstdlib>
12 #include <fstream>
13 #include <iostream>
14 #include <memory>
15 #include <string>
16 #include <utility>
17 #include <vector>
18
19 #include "absl/container/flat_hash_set.h"
20 #include "absl/memory/memory.h"
21 #include "absl/strings/ascii.h"
22 #include "absl/strings/match.h"
23 #include "absl/strings/str_cat.h"
24 #include "absl/strings/str_split.h"
25 #include "absl/strings/string_view.h"
26 #include "absl/strings/strip.h"
27 #include "google/protobuf/compiler/code_generator.h"
28 #include "google/protobuf/compiler/objectivec/file.h"
29 #include "google/protobuf/compiler/objectivec/names.h"
30 #include "google/protobuf/compiler/objectivec/options.h"
31 #include "google/protobuf/descriptor.h"
32 #include "google/protobuf/io/printer.h"
33 #include "google/protobuf/io/zero_copy_stream.h"
34
35 namespace google {
36 namespace protobuf {
37 namespace compiler {
38 namespace objectivec {
39
40 namespace {
41
42 // Convert a string with "yes"/"no" (case insensitive) to a boolean, returning
43 // true/false for if the input string was a valid value. The empty string is
44 // also treated as a true value. If the input string is invalid, `result` is
45 // unchanged.
StringToBool(const std::string & value,bool * result)46 bool StringToBool(const std::string& value, bool* result) {
47 std::string upper_value(value);
48 absl::AsciiStrToUpper(&upper_value);
49 if (upper_value == "NO") {
50 *result = false;
51 return true;
52 }
53 if (upper_value == "YES" || upper_value.empty()) {
54 *result = true;
55 return true;
56 }
57
58 return false;
59 }
60
NumberedObjCMFileName(absl::string_view basename,int number)61 std::string NumberedObjCMFileName(absl::string_view basename, int number) {
62 return absl::StrCat(basename, ".out/", number, ".pbobjc.m");
63 }
64
65 } // namespace
66
HasGenerateAll() const67 bool ObjectiveCGenerator::HasGenerateAll() const { return true; }
68
Generate(const FileDescriptor * file,const std::string & parameter,GeneratorContext * context,std::string * error) const69 bool ObjectiveCGenerator::Generate(const FileDescriptor* file,
70 const std::string& parameter,
71 GeneratorContext* context,
72 std::string* error) const {
73 *error = "Unimplemented Generate() method. Call GenerateAll() instead.";
74 return false;
75 }
76
GenerateAll(const std::vector<const FileDescriptor * > & files,const std::string & parameter,GeneratorContext * context,std::string * error) const77 bool ObjectiveCGenerator::GenerateAll(
78 const std::vector<const FileDescriptor*>& files,
79 const std::string& parameter, GeneratorContext* context,
80 std::string* error) const {
81 // -----------------------------------------------------------------
82 // Parse generator options. These options are passed to the compiler using the
83 // --objc_opt flag. The options are passed as a comma separated list of
84 // options along with their values. If the option appears multiple times, only
85 // the last value will be considered.
86 //
87 // e.g. protoc ...
88 // --objc_opt=expected_prefixes=file.txt,generate_for_named_framework=MyFramework
89
90 Options validation_options;
91 GenerationOptions generation_options;
92
93 std::vector<std::pair<std::string, std::string> > options;
94 ParseGeneratorParameter(parameter, &options);
95 for (size_t i = 0; i < options.size(); i++) {
96 if (options[i].first == "expected_prefixes_path") {
97 // Path to find a file containing the expected prefixes
98 // (objc_class_prefix "PREFIX") for proto packages (package NAME). The
99 // generator will then issue warnings/errors if in the proto files being
100 // generated the option is not listed/wrong/etc in the file.
101 //
102 // The format of the file is:
103 // - An entry is a line of "package=prefix".
104 // - Comments start with "#".
105 // - A comment can go on a line after a expected package/prefix pair.
106 // (i.e. - "package=prefix # comment")
107 // - For files that do NOT have a proto package (not recommended), an
108 // entry can be made as "no_package:PATH=prefix", where PATH is the
109 // path for the .proto file.
110 //
111 // There is no validation that the prefixes are good prefixes, it is
112 // assumed that they are when you create the file.
113 validation_options.expected_prefixes_path = options[i].second;
114 } else if (options[i].first == "expected_prefixes_suppressions") {
115 // A semicolon delimited string that lists the paths of .proto files to
116 // exclude from the package prefix validations (expected_prefixes_path).
117 // This is provided as an "out", to skip some files being checked.
118 for (absl::string_view split_piece :
119 absl::StrSplit(options[i].second, ';', absl::SkipEmpty())) {
120 validation_options.expected_prefixes_suppressions.push_back(
121 std::string(split_piece));
122 }
123 } else if (options[i].first == "prefixes_must_be_registered") {
124 // If objc prefix file option value must be registered to be used. This
125 // option has no meaning if an "expected_prefixes_path" isn't set. The
126 // available options are:
127 // "no": They don't have to be registered.
128 // "yes": They must be registered and an error will be raised if a files
129 // tried to use a prefix that isn't registered.
130 // Default is "no".
131 if (!StringToBool(options[i].second,
132 &validation_options.prefixes_must_be_registered)) {
133 *error = absl::StrCat(
134 "error: Unknown value for prefixes_must_be_registered: ",
135 options[i].second);
136 return false;
137 }
138 } else if (options[i].first == "require_prefixes") {
139 // If every file must have an objc prefix file option to be used. The
140 // available options are:
141 // "no": Files can be generated without the prefix option.
142 // "yes": Files must have the objc prefix option, and an error will be
143 // raised if a files doesn't have one.
144 // Default is "no".
145 if (!StringToBool(options[i].second,
146 &validation_options.require_prefixes)) {
147 *error = absl::StrCat("error: Unknown value for require_prefixes: ",
148 options[i].second);
149 return false;
150 }
151 } else if (options[i].first == "generate_for_named_framework") {
152 // The name of the framework that protos are being generated for. This
153 // will cause the #import statements to be framework based using this
154 // name (i.e. - "#import <NAME/proto.pbobjc.h>).
155 //
156 // NOTE: If this option is used with
157 // named_framework_to_proto_path_mappings_path, then this is effectively
158 // the "default" framework name used for everything that wasn't mapped by
159 // the mapping file.
160 generation_options.generate_for_named_framework = options[i].second;
161 } else if (options[i].first ==
162 "named_framework_to_proto_path_mappings_path") {
163 // Path to find a file containing the list of framework names and proto
164 // files. The generator uses this to decide if a proto file
165 // referenced should use a framework style import vs. a user level import
166 // (#import <FRAMEWORK/file.pbobjc.h> vs #import "dir/file.pbobjc.h").
167 //
168 // The format of the file is:
169 // - An entry is a line of "frameworkName: file.proto, dir/file2.proto".
170 // - Comments start with "#".
171 // - A comment can go on a line after a expected package/prefix pair.
172 // (i.e. - "frameworkName: file.proto # comment")
173 //
174 // Any number of files can be listed for a framework, just separate them
175 // with commas.
176 //
177 // There can be multiple lines listing the same frameworkName in case it
178 // has a lot of proto files included in it; having multiple lines makes
179 // things easier to read. If a proto file is not configured in the
180 // mappings file, it will use the default framework name if one was passed
181 // with generate_for_named_framework, or the relative path to it's include
182 // path otherwise.
183 generation_options.named_framework_to_proto_path_mappings_path =
184 options[i].second;
185 } else if (options[i].first == "runtime_import_prefix") {
186 // Path to use as a prefix on #imports of runtime provided headers in the
187 // generated files. When integrating ObjC protos into a build system,
188 // this can be used to avoid having to add the runtime directory to the
189 // header search path since the generate #import will be more complete.
190 generation_options.runtime_import_prefix =
191 std::string(absl::StripSuffix(options[i].second, "/"));
192 } else if (options[i].first == "package_to_prefix_mappings_path") {
193 // Path to use for when loading the objc class prefix mappings to use.
194 // The `objc_class_prefix` file option is always honored first if one is
195 // present. This option also has precedent over the use_package_as_prefix
196 // option.
197 //
198 // The format of the file is:
199 // - An entry is a line of "package=prefix".
200 // - Comments start with "#".
201 // - A comment can go on a line after a expected package/prefix pair.
202 // (i.e. - "package=prefix # comment")
203 // - For files that do NOT have a proto package (not recommended), an
204 // entry can be made as "no_package:PATH=prefix", where PATH is the
205 // path for the .proto file.
206 //
207 SetPackageToPrefixMappingsPath(options[i].second);
208 } else if (options[i].first == "use_package_as_prefix") {
209 // Controls how the symbols should be prefixed to avoid symbols
210 // collisions. The objc_class_prefix file option is always honored, this
211 // is just what to do if that isn't set. The available options are:
212 // "no": Not prefixed (the existing mode).
213 // "yes": Make a prefix out of the proto package.
214 bool value = false;
215 if (StringToBool(options[i].second, &value)) {
216 SetUseProtoPackageAsDefaultPrefix(value);
217 } else {
218 *error = absl::StrCat("error: Unknown use_package_as_prefix: ",
219 options[i].second);
220 return false;
221 }
222 } else if (options[i].first == "proto_package_prefix_exceptions_path") {
223 // Path to find a file containing the list of proto package names that are
224 // exceptions when use_package_as_prefix is enabled. This can be used to
225 // migrate packages one at a time to use_package_as_prefix since there
226 // are likely code updates needed with each one.
227 //
228 // The format of the file is:
229 // - An entry is a line of "proto.package.name".
230 // - Comments start with "#".
231 // - A comment can go on a line after a expected package/prefix pair.
232 // (i.e. - "some.proto.package # comment")
233 SetProtoPackagePrefixExceptionList(options[i].second);
234 } else if (options[i].first == "package_as_prefix_forced_prefix") {
235 // String to use as the prefix when deriving a prefix from the package
236 // name. So this only applies when use_package_as_prefix is also used.
237 SetForcedPackagePrefix(options[i].second);
238 } else if (options[i].first == "headers_use_forward_declarations") {
239 if (!StringToBool(options[i].second,
240 &generation_options.headers_use_forward_declarations)) {
241 *error = absl::StrCat(
242 "error: Unknown value for headers_use_forward_declarations: ",
243 options[i].second);
244 return false;
245 }
246 } else if (options[i].first == "strip_custom_options") {
247 // Controls if extensions that define custom options are included the
248 // generated code. Since ObjC protos does not capture these descriptor
249 // options, there normally isn't a need for these extensions. Docs on
250 // custom options:
251 // https://protobuf.dev/programming-guides/proto2/#customoptions
252 if (!StringToBool(options[i].second,
253 &generation_options.strip_custom_options)) {
254 *error = absl::StrCat("error: Unknown value for strip_custom_options: ",
255 options[i].second);
256 return false;
257 }
258 } else if (options[i].first == "generate_minimal_imports") {
259 // Controls if minimal imports should be generated from a files imports.
260 // Since custom options require imports, they current cause generated
261 // imports even though there is nothing captured in the generated code,
262 // this provides smaller imports only for the things referenced. This
263 // could break code in complex cases where code uses types via long
264 // import chains with public imports mixed through the way, as things
265 // that aren't really needed for the local usages could be pruned.
266 if (!StringToBool(options[i].second,
267 &generation_options.generate_minimal_imports)) {
268 *error =
269 absl::StrCat("error: Unknown value for generate_minimal_imports: ",
270 options[i].second);
271 return false;
272 }
273 } else if (options[i].first == "experimental_multi_source_generation") {
274 // This is an experimental option, and could be removed or change at any
275 // time; it is not documented in the README.md for that reason.
276 //
277 // Enables a mode where each ObjC class (messages and roots) generates to
278 // a unique .m file; this is to explore impacts on code size when not
279 // compiling/linking with `-ObjC` as then only linker visible needs should
280 // be pulled into the builds.
281 if (!StringToBool(
282 options[i].second,
283 &generation_options.experimental_multi_source_generation)) {
284 *error = absl::StrCat(
285 "error: Unknown value for experimental_multi_source_generation: ",
286 options[i].second);
287 return false;
288 }
289 } else if (options[i].first == "experimental_strip_nonfunctional_codegen") {
290 if (!StringToBool(
291 options[i].second,
292 &generation_options.experimental_strip_nonfunctional_codegen)) {
293 *error = absl::StrCat(
294 "error: Unknown value for "
295 "experimental_strip_nonfunctional_codegen: ",
296 options[i].second);
297 return false;
298 }
299 } else {
300 *error =
301 absl::StrCat("error: Unknown generator option: ", options[i].first);
302 return false;
303 }
304 }
305
306 // Multi source generation forces:
307 // - off the use of fwd decls in favor of imports
308 // - on the minimal imports support
309 if (generation_options.experimental_multi_source_generation) {
310 generation_options.headers_use_forward_declarations = false;
311 generation_options.generate_minimal_imports = true;
312 }
313 if (generation_options.experimental_strip_nonfunctional_codegen) {
314 generation_options.generate_minimal_imports = true;
315 }
316
317 // -----------------------------------------------------------------
318
319 // NOTE: src/google/protobuf/compiler/plugin.cc makes use of cerr for some
320 // error cases, so it seems to be ok to use as a back door for warnings.
321
322 // This is a way to turn off these warnings, the intent is that if you find
323 // this then you also did as asked and filed an issue so the need for the
324 // generation option is known. But it allows you to keep your builds quiet
325 // after opening the issue. The value of the environment variable should be
326 // a comma separated list of the names of the options to suppress their usage
327 // warning.
328 char* options_warnings_suppressions_cstr =
329 getenv("GPB_OBJC_SUPPRESS_DEPRECATED_OPTIONS_WARNINGS");
330 const absl::string_view options_warnings_suppressions =
331 options_warnings_suppressions_cstr != nullptr
332 ? options_warnings_suppressions_cstr
333 : "";
334 if (generation_options.headers_use_forward_declarations &&
335 !absl::StrContains(options_warnings_suppressions,
336 "headers_use_forward_declarations")) {
337 std::cerr << "WARNING: headers_use_forward_declarations is enabled, this "
338 "is deprecated and will be removed in the future. If you have "
339 "a need for enabling it please file an issue at "
340 "https://github.com/protocolbuffers/protobuf/issues with "
341 "your use case."
342 << std::endl;
343 std::cerr.flush();
344 }
345 if (!generation_options.generate_minimal_imports &&
346 !absl::StrContains(options_warnings_suppressions,
347 "generate_minimal_imports")) {
348 std::cerr << "WARNING: generate_minimal_imports is disabled, this is "
349 "deprecated and will be removed in the future. If you have a "
350 "need for disabling it please file an issue at "
351 "https://github.com/protocolbuffers/protobuf/issues with "
352 "your use case."
353 << std::endl;
354 std::cerr.flush();
355 }
356 if (!generation_options.strip_custom_options &&
357 !absl::StrContains(options_warnings_suppressions,
358 "strip_custom_options")) {
359 std::cerr << "WARNING: strip_custom_options is disabled, this is deprecated"
360 "and will be removed in the future. If you have a need for "
361 "disabling it please file an issue at "
362 "https://github.com/protocolbuffers/protobuf/issues with "
363 "your use case."
364 << std::endl;
365 std::cerr.flush();
366 }
367
368 // -----------------------------------------------------------------
369
370 // These are not official generation options and could be removed/changed in
371 // the future and doing that won't count as a breaking change.
372 bool headers_only = getenv("GPB_OBJC_HEADERS_ONLY") != nullptr;
373 absl::flat_hash_set<std::string> skip_impls;
374 if (getenv("GPB_OBJC_SKIP_IMPLS_FILE") != nullptr) {
375 std::ifstream skip_file(getenv("GPB_OBJC_SKIP_IMPLS_FILE"));
376 if (skip_file.is_open()) {
377 std::string line;
378 while (std::getline(skip_file, line)) {
379 skip_impls.insert(line);
380 }
381 } else {
382 *error = "error: Failed to open GPB_OBJC_SKIP_IMPLS_FILE file";
383 return false;
384 }
385 }
386
387 // -----------------------------------------------------------------
388
389 // Validate the objc prefix/package pairings.
390 if (!ValidateObjCClassPrefixes(files, validation_options, error)) {
391 // *error will have been filled in.
392 return false;
393 }
394
395 FileGenerator::CommonState state(!generation_options.strip_custom_options);
396 for (const auto& file : files) {
397 const FileGenerator file_generator(GetEdition(*file), file,
398 generation_options, state);
399 std::string filepath = FilePath(file);
400
401 // Generate header.
402 {
403 auto output =
404 absl::WrapUnique(context->Open(absl::StrCat(filepath, ".pbobjc.h")));
405 io::Printer printer(output.get());
406 file_generator.GenerateHeader(&printer);
407 if (printer.failed()) {
408 *error = absl::StrCat("error: internal error generating a header: ",
409 file->name());
410 return false;
411 }
412 }
413
414 // Generate m file(s).
415 if (!headers_only && skip_impls.count(file->name()) == 0) {
416 if (generation_options.experimental_multi_source_generation) {
417 int file_number = 0;
418
419 // Generate the Root and FileDescriptor (if needed).
420 {
421 std::unique_ptr<io::ZeroCopyOutputStream> output(
422 context->Open(NumberedObjCMFileName(filepath, file_number++)));
423 io::Printer printer(output.get());
424 file_generator.GenerateGlobalSource(&printer);
425 if (printer.failed()) {
426 *error = absl::StrCat(
427 "error: internal error generating an implementation:",
428 file->name());
429 return false;
430 }
431 }
432
433 // Enums only generate C functions, so they can all go in one file as
434 // dead stripping anything not used.
435 if (file_generator.NumEnums() > 0) {
436 std::unique_ptr<io::ZeroCopyOutputStream> output(
437 context->Open(NumberedObjCMFileName(filepath, file_number++)));
438 io::Printer printer(output.get());
439 file_generator.GenerateSourceForEnums(&printer);
440 if (printer.failed()) {
441 *error = absl::StrCat(
442 "error: internal error generating an enum implementation(s):",
443 file->name());
444 return false;
445 }
446 }
447
448 for (int i = 0; i < file_generator.NumMessages(); ++i) {
449 std::unique_ptr<io::ZeroCopyOutputStream> output(
450 context->Open(NumberedObjCMFileName(filepath, file_number++)));
451 io::Printer printer(output.get());
452 file_generator.GenerateSourceForMessage(i, &printer);
453 if (printer.failed()) {
454 *error = absl::StrCat(
455 "error: internal error generating an message implementation:",
456 file->name(), "::", i);
457 return false;
458 }
459 }
460 } else {
461 auto output = absl::WrapUnique(
462 context->Open(absl::StrCat(filepath, ".pbobjc.m")));
463 io::Printer printer(output.get());
464 file_generator.GenerateSource(&printer);
465 if (printer.failed()) {
466 *error = absl::StrCat(
467 "error: internal error generating an implementation:",
468 file->name());
469 return false;
470 }
471 }
472 } // if (!headers_only && skip_impls.count(file->name()) == 0)
473 } // for(file : files)
474
475 return true;
476 }
477
478 } // namespace objectivec
479 } // namespace compiler
480 } // namespace protobuf
481 } // namespace google
482