codekingpro/portable-devtools
114k
1# Copyright (c) 2012 Google Inc. All rights reserved.
2# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
4
5
6import errno
7import filecmp
8import os
9import posixpath
10import re
11import shutil
12import subprocess
13import sys
14import tempfile
15
16import gyp.common
17import gyp.xcode_ninja
18import gyp.xcodeproj_file
19
20# Project files generated by this module will use _intermediate_var as a
21# custom Xcode setting whose value is a DerivedSources-like directory that's
22# project-specific and configuration-specific. The normal choice,
23# DERIVED_FILE_DIR, is target-specific, which is thought to be too restrictive
24# as it is likely that multiple targets within a single project file will want
25# to access the same set of generated files. The other option,
26# PROJECT_DERIVED_FILE_DIR, is unsuitable because while it is project-specific,
27# it is not configuration-specific. INTERMEDIATE_DIR is defined as
28# $(PROJECT_DERIVED_FILE_DIR)/$(CONFIGURATION).
29_intermediate_var = "INTERMEDIATE_DIR"
30
31# SHARED_INTERMEDIATE_DIR is the same, except that it is shared among all
32# targets that share the same BUILT_PRODUCTS_DIR.
33_shared_intermediate_var = "SHARED_INTERMEDIATE_DIR"
34
35_library_search_paths_var = "LIBRARY_SEARCH_PATHS"
36
37generator_default_variables = {
38 "EXECUTABLE_PREFIX": "",
39 "EXECUTABLE_SUFFIX": "",
40 "STATIC_LIB_PREFIX": "lib",
41 "SHARED_LIB_PREFIX": "lib",
42 "STATIC_LIB_SUFFIX": ".a",
43 "SHARED_LIB_SUFFIX": ".dylib",
44 # INTERMEDIATE_DIR is a place for targets to build up intermediate products.
45 # It is specific to each build environment. It is only guaranteed to exist
46 # and be constant within the context of a project, corresponding to a single
47 # input file. Some build environments may allow their intermediate directory
48 # to be shared on a wider scale, but this is not guaranteed.
49 "INTERMEDIATE_DIR": "$(%s)" % _intermediate_var,
50 "OS": "mac",
51 "PRODUCT_DIR": "$(BUILT_PRODUCTS_DIR)",
52 "LIB_DIR": "$(BUILT_PRODUCTS_DIR)",
53 "RULE_INPUT_ROOT": "$(INPUT_FILE_BASE)",
54 "RULE_INPUT_EXT": "$(INPUT_FILE_SUFFIX)",
55 "RULE_INPUT_NAME": "$(INPUT_FILE_NAME)",
56 "RULE_INPUT_PATH": "$(INPUT_FILE_PATH)",
57 "RULE_INPUT_DIRNAME": "$(INPUT_FILE_DIRNAME)",
58 "SHARED_INTERMEDIATE_DIR": "$(%s)" % _shared_intermediate_var,
59 "CONFIGURATION_NAME": "$(CONFIGURATION)",
60}
61
62# The Xcode-specific sections that hold paths.
63generator_additional_path_sections = [
64 "mac_bundle_resources",
65 "mac_framework_headers",
66 "mac_framework_private_headers",
67 # 'mac_framework_dirs', input already handles _dirs endings.
68]
69
70# The Xcode-specific keys that exist on targets and aren't moved down to
71# configurations.
72generator_additional_non_configuration_keys = [
73 "ios_app_extension",
74 "ios_watch_app",
75 "ios_watchkit_extension",
76 "mac_bundle",
77 "mac_bundle_resources",
78 "mac_framework_headers",
79 "mac_framework_private_headers",
80 "mac_xctest_bundle",
81 "mac_xcuitest_bundle",
82 "xcode_create_dependents_test_runner",
83]
84
85# We want to let any rules apply to files that are resources also.
86generator_extra_sources_for_rules = [
87 "mac_bundle_resources",
88 "mac_framework_headers",
89 "mac_framework_private_headers",
90]
91
92generator_filelist_paths = None
93
94# Xcode's standard set of library directories, which don't need to be duplicated
95# in LIBRARY_SEARCH_PATHS. This list is not exhaustive, but that's okay.
96xcode_standard_library_dirs = frozenset(
97 ["$(SDKROOT)/usr/lib", "$(SDKROOT)/usr/local/lib"]
98)
99
100
101def CreateXCConfigurationList(configuration_names):
102 xccl = gyp.xcodeproj_file.XCConfigurationList({"buildConfigurations": []})
103 if len(configuration_names) == 0:
104 configuration_names = ["Default"]
105 for configuration_name in configuration_names:
106 xcbc = gyp.xcodeproj_file.XCBuildConfiguration({"name": configuration_name})
107 xccl.AppendProperty("buildConfigurations", xcbc)
108 xccl.SetProperty("defaultConfigurationName", configuration_names[0])
109 return xccl
110
111
112class XcodeProject:
113 def __init__(self, gyp_path, path, build_file_dict):
114 self.gyp_path = gyp_path
115 self.path = path
116 self.project = gyp.xcodeproj_file.PBXProject(path=path)
117 projectDirPath = gyp.common.RelativePath(
118 os.path.dirname(os.path.abspath(self.gyp_path)),
119 os.path.dirname(path) or ".",
120 )
121 self.project.SetProperty("projectDirPath", projectDirPath)
122 self.project_file = gyp.xcodeproj_file.XCProjectFile(
123 {"rootObject": self.project}
124 )
125 self.build_file_dict = build_file_dict
126
127 # TODO(mark): add destructor that cleans up self.path if created_dir is
128 # True and things didn't complete successfully. Or do something even
129 # better with "try"?
130 self.created_dir = False
131 try:
132 os.makedirs(self.path)
133 self.created_dir = True
134 except OSError as e:
135 if e.errno != errno.EEXIST:
136 raise
137
138 def Finalize1(self, xcode_targets, serialize_all_tests):
139 # Collect a list of all of the build configuration names used by the
140 # various targets in the file. It is very heavily advised to keep each
141 # target in an entire project (even across multiple project files) using
142 # the same set of configuration names.
143 configurations = []
144 for xct in self.project.GetProperty("targets"):
145 xccl = xct.GetProperty("buildConfigurationList")
146 xcbcs = xccl.GetProperty("buildConfigurations")
147 for xcbc in xcbcs:
148 name = xcbc.GetProperty("name")
149 if name not in configurations:
150 configurations.append(name)
151
152 # Replace the XCConfigurationList attached to the PBXProject object with
153 # a new one specifying all of the configuration names used by the various
154 # targets.
155 try:
156 xccl = CreateXCConfigurationList(configurations)
157 self.project.SetProperty("buildConfigurationList", xccl)
158 except Exception:
159 sys.stderr.write("Problem with gyp file %s\n" % self.gyp_path)
160 raise
161
162 # The need for this setting is explained above where _intermediate_var is
163 # defined. The comments below about wanting to avoid project-wide build
164 # settings apply here too, but this needs to be set on a project-wide basis
165 # so that files relative to the _intermediate_var setting can be displayed
166 # properly in the Xcode UI.
167 #
168 # Note that for configuration-relative files such as anything relative to
169 # _intermediate_var, for the purposes of UI tree view display, Xcode will
170 # only resolve the configuration name once, when the project file is
171 # opened. If the active build configuration is changed, the project file
172 # must be closed and reopened if it is desired for the tree view to update.
173 # This is filed as Apple radar 6588391.
174 xccl.SetBuildSetting(
175 _intermediate_var, "$(PROJECT_DERIVED_FILE_DIR)/$(CONFIGURATION)"
176 )
177 xccl.SetBuildSetting(
178 _shared_intermediate_var, "$(SYMROOT)/DerivedSources/$(CONFIGURATION)"
179 )
180
181 # Set user-specified project-wide build settings and config files. This
182 # is intended to be used very sparingly. Really, almost everything should
183 # go into target-specific build settings sections. The project-wide
184 # settings are only intended to be used in cases where Xcode attempts to
185 # resolve variable references in a project context as opposed to a target
186 # context, such as when resolving sourceTree references while building up
187 # the tree tree view for UI display.
188 # Any values set globally are applied to all configurations, then any
189 # per-configuration values are applied.
190 for xck, xcv in self.build_file_dict.get("xcode_settings", {}).items():
191 xccl.SetBuildSetting(xck, xcv)
192 if "xcode_config_file" in self.build_file_dict:
193 config_ref = self.project.AddOrGetFileInRootGroup(
194 self.build_file_dict["xcode_config_file"]
195 )
196 xccl.SetBaseConfiguration(config_ref)
197 build_file_configurations = self.build_file_dict.get("configurations", {})
198 if build_file_configurations:
199 for config_name in configurations:
200 build_file_configuration_named = build_file_configurations.get(
201 config_name, {}
202 )
203 if build_file_configuration_named:
204 xcc = xccl.ConfigurationNamed(config_name)
205 for xck, xcv in build_file_configuration_named.get(
206 "xcode_settings", {}
207 ).items():
208 xcc.SetBuildSetting(xck, xcv)
209 if "xcode_config_file" in build_file_configuration_named:
210 config_ref = self.project.AddOrGetFileInRootGroup(
211 build_file_configurations[config_name]["xcode_config_file"]
212 )
213 xcc.SetBaseConfiguration(config_ref)
214
215 # Sort the targets based on how they appeared in the input.
216 # TODO(mark): Like a lot of other things here, this assumes internal
217 # knowledge of PBXProject - in this case, of its "targets" property.
218
219 # ordinary_targets are ordinary targets that are already in the project
220 # file. run_test_targets are the targets that run unittests and should be
221 # used for the Run All Tests target. support_targets are the action/rule
222 # targets used by GYP file targets, just kept for the assert check.
223 ordinary_targets = []
224 run_test_targets = []
225 support_targets = []
226
227 # targets is full list of targets in the project.
228 targets = []
229
230 # does the it define it's own "all"?
231 has_custom_all = False
232
233 # targets_for_all is the list of ordinary_targets that should be listed
234 # in this project's "All" target. It includes each non_runtest_target
235 # that does not have suppress_wildcard set.
236 targets_for_all = []
237
238 for target in self.build_file_dict["targets"]:
239 target_name = target["target_name"]
240 toolset = target["toolset"]
241 qualified_target = gyp.common.QualifiedTarget(
242 self.gyp_path, target_name, toolset
243 )
244 xcode_target = xcode_targets[qualified_target]
245 # Make sure that the target being added to the sorted list is already in
246 # the unsorted list.
247 assert xcode_target in self.project._properties["targets"]
248 targets.append(xcode_target)
249 ordinary_targets.append(xcode_target)
250 if xcode_target.support_target:
251 support_targets.append(xcode_target.support_target)
252 targets.append(xcode_target.support_target)
253
254 if not int(target.get("suppress_wildcard", False)):
255 targets_for_all.append(xcode_target)
256
257 if target_name.lower() == "all":
258 has_custom_all = True
259
260 # If this target has a 'run_as' attribute, add its target to the
261 # targets, and add it to the test targets.
262 if target.get("run_as"):
263 # Make a target to run something. It should have one
264 # dependency, the parent xcode target.
265 xccl = CreateXCConfigurationList(configurations)
266 run_target = gyp.xcodeproj_file.PBXAggregateTarget(
267 {
268 "name": "Run " + target_name,
269 "productName": xcode_target.GetProperty("productName"),
270 "buildConfigurationList": xccl,
271 },
272 parent=self.project,
273 )
274 run_target.AddDependency(xcode_target)
275
276 command = target["run_as"]
277 script = ""
278 if command.get("working_directory"):
279 script = (
280 script
281 + 'cd "%s"\n'
282 % gyp.xcodeproj_file.ConvertVariablesToShellSyntax(
283 command.get("working_directory")
284 )
285 )
286
287 if command.get("environment"):
288 script = (
289 script
290 + "\n".join(
291 [
292 'export %s="%s"'
293 % (
294 key,
295 gyp.xcodeproj_file.ConvertVariablesToShellSyntax(
296 val
297 ),
298 )
299 for (key, val) in command.get("environment").items()
300 ]
301 )
302 + "\n"
303 )
304
305 # Some test end up using sockets, files on disk, etc. and can get
306 # confused if more then one test runs at a time. The generator
307 # flag 'xcode_serialize_all_test_runs' controls the forcing of all
308 # tests serially. It defaults to True. To get serial runs this
309 # little bit of python does the same as the linux flock utility to
310 # make sure only one runs at a time.
311 command_prefix = ""
312 if serialize_all_tests:
313 command_prefix = """python -c "import fcntl, subprocess, sys
314file = open('$TMPDIR/GYP_serialize_test_runs', 'a')
315fcntl.flock(file.fileno(), fcntl.LOCK_EX)
316sys.exit(subprocess.call(sys.argv[1:]))" """
317
318 # If we were unable to exec for some reason, we want to exit
319 # with an error, and fixup variable references to be shell
320 # syntax instead of xcode syntax.
321 script = (
322 script
323 + "exec "
324 + command_prefix
325 + "%s\nexit 1\n"
326 % gyp.xcodeproj_file.ConvertVariablesToShellSyntax(
327 gyp.common.EncodePOSIXShellList(command.get("action"))
328 )
329 )
330
331 ssbp = gyp.xcodeproj_file.PBXShellScriptBuildPhase(
332 {"shellScript": script, "showEnvVarsInLog": 0}
333 )
334 run_target.AppendProperty("buildPhases", ssbp)
335
336 # Add the run target to the project file.
337 targets.append(run_target)
338 run_test_targets.append(run_target)
339 xcode_target.test_runner = run_target
340
341 # Make sure that the list of targets being replaced is the same length as
342 # the one replacing it, but allow for the added test runner targets.
343 assert len(self.project._properties["targets"]) == len(ordinary_targets) + len(
344 support_targets
345 )
346
347 self.project._properties["targets"] = targets
348
349 # Get rid of unnecessary levels of depth in groups like the Source group.
350 self.project.RootGroupsTakeOverOnlyChildren(True)
351
352 # Sort the groups nicely. Do this after sorting the targets, because the
353 # Products group is sorted based on the order of the targets.
354 self.project.SortGroups()
355
356 # Create an "All" target if there's more than one target in this project
357 # file and the project didn't define its own "All" target. Put a generated
358 # "All" target first so that people opening up the project for the first
359 # time will build everything by default.
360 if len(targets_for_all) > 1 and not has_custom_all:
361 xccl = CreateXCConfigurationList(configurations)
362 all_target = gyp.xcodeproj_file.PBXAggregateTarget(
363 {"buildConfigurationList": xccl, "name": "All"}, parent=self.project
364 )
365
366 for target in targets_for_all:
367 all_target.AddDependency(target)
368
369 # TODO(mark): This is evil because it relies on internal knowledge of
370 # PBXProject._properties. It's important to get the "All" target first,
371 # though.
372 self.project._properties["targets"].insert(0, all_target)
373
374 # The same, but for run_test_targets.
375 if len(run_test_targets) > 1:
376 xccl = CreateXCConfigurationList(configurations)
377 run_all_tests_target = gyp.xcodeproj_file.PBXAggregateTarget(
378 {"buildConfigurationList": xccl, "name": "Run All Tests"},
379 parent=self.project,
380 )
381 for run_test_target in run_test_targets:
382 run_all_tests_target.AddDependency(run_test_target)
383
384 # Insert after the "All" target, which must exist if there is more than
385 # one run_test_target.
386 self.project._properties["targets"].insert(1, run_all_tests_target)
387
388 def Finalize2(self, xcode_targets, xcode_target_to_target_dict):
389 # Finalize2 needs to happen in a separate step because the process of
390 # updating references to other projects depends on the ordering of targets
391 # within remote project files. Finalize1 is responsible for sorting duty,
392 # and once all project files are sorted, Finalize2 can come in and update
393 # these references.
394
395 # To support making a "test runner" target that will run all the tests
396 # that are direct dependents of any given target, we look for
397 # xcode_create_dependents_test_runner being set on an Aggregate target,
398 # and generate a second target that will run the tests runners found under
399 # the marked target.
400 for bf_tgt in self.build_file_dict["targets"]:
401 if int(bf_tgt.get("xcode_create_dependents_test_runner", 0)):
402 tgt_name = bf_tgt["target_name"]
403 toolset = bf_tgt["toolset"]
404 qualified_target = gyp.common.QualifiedTarget(
405 self.gyp_path, tgt_name, toolset
406 )
407 xcode_target = xcode_targets[qualified_target]
408 if isinstance(xcode_target, gyp.xcodeproj_file.PBXAggregateTarget):
409 # Collect all the run test targets.
410 all_run_tests = []
411 pbxtds = xcode_target.GetProperty("dependencies")
412 for pbxtd in pbxtds:
413 pbxcip = pbxtd.GetProperty("targetProxy")
414 dependency_xct = pbxcip.GetProperty("remoteGlobalIDString")
415 if hasattr(dependency_xct, "test_runner"):
416 all_run_tests.append(dependency_xct.test_runner)
417
418 # Directly depend on all the runners as they depend on the target
419 # that builds them.
420 if len(all_run_tests) > 0:
421 run_all_target = gyp.xcodeproj_file.PBXAggregateTarget(
422 {
423 "name": "Run %s Tests" % tgt_name,
424 "productName": tgt_name,
425 },
426 parent=self.project,
427 )
428 for run_test_target in all_run_tests:
429 run_all_target.AddDependency(run_test_target)
430
431 # Insert the test runner after the related target.
432 idx = self.project._properties["targets"].index(xcode_target)
433 self.project._properties["targets"].insert(
434 idx + 1, run_all_target
435 )
436
437 # Update all references to other projects, to make sure that the lists of
438 # remote products are complete. Otherwise, Xcode will fill them in when
439 # it opens the project file, which will result in unnecessary diffs.
440 # TODO(mark): This is evil because it relies on internal knowledge of
441 # PBXProject._other_pbxprojects.
442 for other_pbxproject in self.project._other_pbxprojects:
443 self.project.AddOrGetProjectReference(other_pbxproject)
444
445 self.project.SortRemoteProductReferences()
446
447 # Give everything an ID.
448 self.project_file.ComputeIDs()
449
450 # Make sure that no two objects in the project file have the same ID. If
451 # multiple objects wind up with the same ID, upon loading the file, Xcode
452 # will only recognize one object (the last one in the file?) and the
453 # results are unpredictable.
454 self.project_file.EnsureNoIDCollisions()
455
456 def Write(self):
457 # Write the project file to a temporary location first. Xcode watches for
458 # changes to the project file and presents a UI sheet offering to reload
459 # the project when it does change. However, in some cases, especially when
460 # multiple projects are open or when Xcode is busy, things don't work so
461 # seamlessly. Sometimes, Xcode is able to detect that a project file has
462 # changed but can't unload it because something else is referencing it.
463 # To mitigate this problem, and to avoid even having Xcode present the UI
464 # sheet when an open project is rewritten for inconsequential changes, the
465 # project file is written to a temporary file in the xcodeproj directory
466 # first. The new temporary file is then compared to the existing project
467 # file, if any. If they differ, the new file replaces the old; otherwise,
468 # the new project file is simply deleted. Xcode properly detects a file
469 # being renamed over an open project file as a change and so it remains
470 # able to present the "project file changed" sheet under this system.
471 # Writing to a temporary file first also avoids the possible problem of
472 # Xcode rereading an incomplete project file.
473 (output_fd, new_pbxproj_path) = tempfile.mkstemp(
474 suffix=".tmp", prefix="project.pbxproj.gyp.", dir=self.path
475 )
476
477 try:
478 output_file = os.fdopen(output_fd, "w")
479
480 self.project_file.Print(output_file)
481 output_file.close()
482
483 pbxproj_path = os.path.join(self.path, "project.pbxproj")
484
485 same = False
486 try:
487 same = filecmp.cmp(pbxproj_path, new_pbxproj_path, False)
488 except OSError as e:
489 if e.errno != errno.ENOENT:
490 raise
491
492 if same:
493 # The new file is identical to the old one, just get rid of the new
494 # one.
495 os.unlink(new_pbxproj_path)
496 else:
497 # The new file is different from the old one, or there is no old one.
498 # Rename the new file to the permanent name.
499 #
500 # tempfile.mkstemp uses an overly restrictive mode, resulting in a
501 # file that can only be read by the owner, regardless of the umask.
502 # There's no reason to not respect the umask here, which means that
503 # an extra hoop is required to fetch it and reset the new file's mode.
504 #
505 # No way to get the umask without setting a new one? Set a safe one
506 # and then set it back to the old value.
507 umask = os.umask(0o77)
508 os.umask(umask)
509
510 os.chmod(new_pbxproj_path, 0o666 & ~umask)
511 os.rename(new_pbxproj_path, pbxproj_path)
512
513 except Exception:
514 # Don't leave turds behind. In fact, if this code was responsible for
515 # creating the xcodeproj directory, get rid of that too.
516 os.unlink(new_pbxproj_path)
517 if self.created_dir:
518 shutil.rmtree(self.path, True)
519 raise
520
521
522def AddSourceToTarget(source, type, pbxp, xct):
523 # TODO(mark): Perhaps source_extensions and library_extensions can be made a
524 # little bit fancier.
525 source_extensions = ["c", "cc", "cpp", "cxx", "m", "mm", "s", "swift"]
526
527 # .o is conceptually more of a "source" than a "library," but Xcode thinks
528 # of "sources" as things to compile and "libraries" (or "frameworks") as
529 # things to link with. Adding an object file to an Xcode target's frameworks
530 # phase works properly.
531 library_extensions = ["a", "dylib", "framework", "o"]
532
533 basename = posixpath.basename(source)
534 (_root, ext) = posixpath.splitext(basename)
535 if ext:
536 ext = ext[1:].lower()
537
538 if ext in source_extensions and type != "none":
539 xct.SourcesPhase().AddFile(source)
540 elif ext in library_extensions and type != "none":
541 xct.FrameworksPhase().AddFile(source)
542 else:
543 # Files that aren't added to a sources or frameworks build phase can still
544 # go into the project file, just not as part of a build phase.
545 pbxp.AddOrGetFileInRootGroup(source)
546
547
548def AddResourceToTarget(resource, pbxp, xct):
549 # TODO(mark): Combine with AddSourceToTarget above? Or just inline this call
550 # where it's used.
551 xct.ResourcesPhase().AddFile(resource)
552
553
554def AddHeaderToTarget(header, pbxp, xct, is_public):
555 # TODO(mark): Combine with AddSourceToTarget above? Or just inline this call
556 # where it's used.
557 settings = "{ATTRIBUTES = (%s, ); }" % ("Private", "Public")[is_public]
558 xct.HeadersPhase().AddFile(header, settings)
559
560
561_xcode_variable_re = re.compile(r"(\$\((.*?)\))")
562
563
564def ExpandXcodeVariables(string, expansions):
565 """Expands Xcode-style $(VARIABLES) in string per the expansions dict.
566
567 In some rare cases, it is appropriate to expand Xcode variables when a
568 project file is generated. For any substring $(VAR) in string, if VAR is a
569 key in the expansions dict, $(VAR) will be replaced with expansions[VAR].
570 Any $(VAR) substring in string for which VAR is not a key in the expansions
571 dict will remain in the returned string.
572 """
573
574 matches = _xcode_variable_re.findall(string)
575 if matches is None:
576 return string
577
578 matches.reverse()
579 for match in matches:
580 (to_replace, variable) = match
581 if variable not in expansions:
582 continue
583
584 replacement = expansions[variable]
585 string = re.sub(re.escape(to_replace), replacement, string)
586
587 return string
588
589
590_xcode_define_re = re.compile(r"([\\\"\' ])")
591
592
593def EscapeXcodeDefine(s):
594 """We must escape the defines that we give to XCode so that it knows not to
595 split on spaces and to respect backslash and quote literals. However, we
596 must not quote the define, or Xcode will incorrectly interpret variables
597 especially $(inherited)."""
598 return re.sub(_xcode_define_re, r"\\\1", s)
599
600
601def PerformBuild(data, configurations, params):
602 options = params["options"]
603
604 for build_file, build_file_dict in data.items():
605 (build_file_root, build_file_ext) = os.path.splitext(build_file)
606 if build_file_ext != ".gyp":
607 continue
608 xcodeproj_path = build_file_root + options.suffix + ".xcodeproj"
609 if options.generator_output:
610 xcodeproj_path = os.path.join(options.generator_output, xcodeproj_path)
611
612 for config in configurations:
613 arguments = ["xcodebuild", "-project", xcodeproj_path]
614 arguments += ["-configuration", config]
615 print(f"Building [{config}]: {arguments}")
616 subprocess.check_call(arguments)
617
618
619def CalculateGeneratorInputInfo(params):
620 toplevel = params["options"].toplevel_dir
621 if params.get("flavor") == "ninja":
622 generator_dir = os.path.relpath(params["options"].generator_output or ".")
623 output_dir = params.get("generator_flags", {}).get("output_dir", "out")
624 output_dir = os.path.normpath(os.path.join(generator_dir, output_dir))
625 qualified_out_dir = os.path.normpath(
626 os.path.join(toplevel, output_dir, "gypfiles-xcode-ninja")
627 )
628 else:
629 output_dir = os.path.normpath(os.path.join(toplevel, "xcodebuild"))
630 qualified_out_dir = os.path.normpath(
631 os.path.join(toplevel, output_dir, "gypfiles")
632 )
633
634 global generator_filelist_paths
635 generator_filelist_paths = {
636 "toplevel": toplevel,
637 "qualified_out_dir": qualified_out_dir,
638 }
639
640
641def GenerateOutput(target_list, target_dicts, data, params):
642 # Optionally configure each spec to use ninja as the external builder.
643 ninja_wrapper = params.get("flavor") == "ninja"
644 if ninja_wrapper:
645 (target_list, target_dicts, data) = gyp.xcode_ninja.CreateWrapper(
646 target_list, target_dicts, data, params
647 )
648
649 options = params["options"]
650 generator_flags = params.get("generator_flags", {})
651 parallel_builds = generator_flags.get("xcode_parallel_builds", True)
652 serialize_all_tests = generator_flags.get("xcode_serialize_all_test_runs", True)
653 upgrade_check_project_version = generator_flags.get(
654 "xcode_upgrade_check_project_version", None
655 )
656
657 # Format upgrade_check_project_version with leading zeros as needed.
658 if upgrade_check_project_version:
659 upgrade_check_project_version = str(upgrade_check_project_version)
660 while len(upgrade_check_project_version) < 4:
661 upgrade_check_project_version = "0" + upgrade_check_project_version
662
663 skip_excluded_files = not generator_flags.get("xcode_list_excluded_files", True)
664 xcode_projects = {}
665 for build_file, build_file_dict in data.items():
666 (build_file_root, build_file_ext) = os.path.splitext(build_file)
667 if build_file_ext != ".gyp":
668 continue
669 xcodeproj_path = build_file_root + options.suffix + ".xcodeproj"
670 if options.generator_output:
671 xcodeproj_path = os.path.join(options.generator_output, xcodeproj_path)
672 xcp = XcodeProject(build_file, xcodeproj_path, build_file_dict)
673 xcode_projects[build_file] = xcp
674 pbxp = xcp.project
675
676 # Set project-level attributes from multiple options
677 project_attributes = {}
678 if parallel_builds:
679 project_attributes["BuildIndependentTargetsInParallel"] = "YES"
680 if upgrade_check_project_version:
681 project_attributes["LastUpgradeCheck"] = upgrade_check_project_version
682 project_attributes["LastTestingUpgradeCheck"] = (
683 upgrade_check_project_version
684 )
685 project_attributes["LastSwiftUpdateCheck"] = upgrade_check_project_version
686 pbxp.SetProperty("attributes", project_attributes)
687
688 # Add gyp/gypi files to project
689 if not generator_flags.get("standalone"):
690 main_group = pbxp.GetProperty("mainGroup")
691 build_group = gyp.xcodeproj_file.PBXGroup({"name": "Build"})
692 main_group.AppendChild(build_group)
693 for included_file in build_file_dict["included_files"]:
694 build_group.AddOrGetFileByPath(included_file, False)
695
696 xcode_targets = {}
697 xcode_target_to_target_dict = {}
698 for qualified_target in target_list:
699 [build_file, target_name, _toolset] = gyp.common.ParseQualifiedTarget(
700 qualified_target
701 )
702
703 spec = target_dicts[qualified_target]
704 if spec["toolset"] != "target":
705 raise Exception(
706 "Multiple toolsets not supported in xcode build (target %s)"
707 % qualified_target
708 )
709 configuration_names = [spec["default_configuration"]]
710 for configuration_name in sorted(spec["configurations"].keys()):
711 if configuration_name not in configuration_names:
712 configuration_names.append(configuration_name)
713 xcp = xcode_projects[build_file]
714 pbxp = xcp.project
715
716 # Set up the configurations for the target according to the list of names
717 # supplied.
718 xccl = CreateXCConfigurationList(configuration_names)
719
720 # Create an XCTarget subclass object for the target. The type with
721 # "+bundle" appended will be used if the target has "mac_bundle" set.
722 # loadable_modules not in a mac_bundle are mapped to
723 # com.googlecode.gyp.xcode.bundle, a pseudo-type that xcode.py interprets
724 # to create a single-file mh_bundle.
725 _types = {
726 "executable": "com.apple.product-type.tool",
727 "loadable_module": "com.googlecode.gyp.xcode.bundle",
728 "shared_library": "com.apple.product-type.library.dynamic",
729 "static_library": "com.apple.product-type.library.static",
730 "mac_kernel_extension": "com.apple.product-type.kernel-extension",
731 "executable+bundle": "com.apple.product-type.application",
732 "loadable_module+bundle": "com.apple.product-type.bundle",
733 "loadable_module+xctest": "com.apple.product-type.bundle.unit-test",
734 "loadable_module+xcuitest": "com.apple.product-type.bundle.ui-testing",
735 "shared_library+bundle": "com.apple.product-type.framework",
736 "executable+extension+bundle": "com.apple.product-type.app-extension",
737 "executable+watch+extension+bundle": "com.apple.product-type.watchkit-extension", # noqa: E501
738 "executable+watch+bundle": "com.apple.product-type.application.watchapp",
739 "mac_kernel_extension+bundle": "com.apple.product-type.kernel-extension",
740 }
741
742 target_properties = {
743 "buildConfigurationList": xccl,
744 "name": target_name,
745 }
746
747 type = spec["type"]
748 is_xctest = int(spec.get("mac_xctest_bundle", 0))
749 is_xcuitest = int(spec.get("mac_xcuitest_bundle", 0))
750 is_bundle = int(spec.get("mac_bundle", 0)) or is_xctest
751 is_app_extension = int(spec.get("ios_app_extension", 0))
752 is_watchkit_extension = int(spec.get("ios_watchkit_extension", 0))
753 is_watch_app = int(spec.get("ios_watch_app", 0))
754 if type != "none":
755 type_bundle_key = type
756 if is_xcuitest:
757 type_bundle_key += "+xcuitest"
758 assert type == "loadable_module", (
759 "mac_xcuitest_bundle targets must have type loadable_module "
760 "(target %s)" % target_name
761 )
762 elif is_xctest:
763 type_bundle_key += "+xctest"
764 assert type == "loadable_module", (
765 "mac_xctest_bundle targets must have type loadable_module "
766 "(target %s)" % target_name
767 )
768 elif is_app_extension:
769 assert is_bundle, (
770 "ios_app_extension flag requires mac_bundle "
771 "(target %s)" % target_name
772 )
773 type_bundle_key += "+extension+bundle"
774 elif is_watchkit_extension:
775 assert is_bundle, (
776 "ios_watchkit_extension flag requires mac_bundle "
777 "(target %s)" % target_name
778 )
779 type_bundle_key += "+watch+extension+bundle"
780 elif is_watch_app:
781 assert is_bundle, (
782 "ios_watch_app flag requires mac_bundle (target %s)" % target_name
783 )
784 type_bundle_key += "+watch+bundle"
785 elif is_bundle:
786 type_bundle_key += "+bundle"
787
788 xctarget_type = gyp.xcodeproj_file.PBXNativeTarget
789 try:
790 target_properties["productType"] = _types[type_bundle_key]
791 except KeyError as e:
792 gyp.common.ExceptionAppend(
793 e,
794 "-- unknown product type while writing target %s" % target_name,
795 )
796 raise
797 else:
798 xctarget_type = gyp.xcodeproj_file.PBXAggregateTarget
799 assert not is_bundle, (
800 'mac_bundle targets cannot have type none (target "%s")' % target_name
801 )
802 assert not is_xcuitest, (
803 'mac_xcuitest_bundle targets cannot have type none (target "%s")'
804 % target_name
805 )
806 assert not is_xctest, (
807 'mac_xctest_bundle targets cannot have type none (target "%s")'
808 % target_name
809 )
810
811 target_product_name = spec.get("product_name")
812 if target_product_name is not None:
813 target_properties["productName"] = target_product_name
814
815 xct = xctarget_type(
816 target_properties,
817 parent=pbxp,
818 force_outdir=spec.get("product_dir"),
819 force_prefix=spec.get("product_prefix"),
820 force_extension=spec.get("product_extension"),
821 )
822 pbxp.AppendProperty("targets", xct)
823 xcode_targets[qualified_target] = xct
824 xcode_target_to_target_dict[xct] = spec
825
826 spec_actions = spec.get("actions", [])
827 spec_rules = spec.get("rules", [])
828
829 # Xcode has some "issues" with checking dependencies for the "Compile
830 # sources" step with any source files/headers generated by actions/rules.
831 # To work around this, if a target is building anything directly (not
832 # type "none"), then a second target is used to run the GYP actions/rules
833 # and is made a dependency of this target. This way the work is done
834 # before the dependency checks for what should be recompiled.
835 support_xct = None
836 # The Xcode "issues" don't affect xcode-ninja builds, since the dependency
837 # logic all happens in ninja. Don't bother creating the extra targets in
838 # that case.
839 if type != "none" and (spec_actions or spec_rules) and not ninja_wrapper:
840 support_xccl = CreateXCConfigurationList(configuration_names)
841 support_target_suffix = generator_flags.get(
842 "support_target_suffix", " Support"
843 )
844 support_target_properties = {
845 "buildConfigurationList": support_xccl,
846 "name": target_name + support_target_suffix,
847 }
848 if target_product_name:
849 support_target_properties["productName"] = (
850 target_product_name + " Support"
851 )
852 support_xct = gyp.xcodeproj_file.PBXAggregateTarget(
853 support_target_properties, parent=pbxp
854 )
855 pbxp.AppendProperty("targets", support_xct)
856 xct.AddDependency(support_xct)
857 # Hang the support target off the main target so it can be tested/found
858 # by the generator during Finalize.
859 xct.support_target = support_xct
860
861 prebuild_index = 0
862
863 # Add custom shell script phases for "actions" sections.
864 for action in spec_actions:
865 # There's no need to write anything into the script to ensure that the
866 # output directories already exist, because Xcode will look at the
867 # declared outputs and automatically ensure that they exist for us.
868
869 # Do we have a message to print when this action runs?
870 message = action.get("message")
871 if message:
872 message = "echo note: " + gyp.common.EncodePOSIXShellArgument(message)
873 else:
874 message = ""
875
876 # Turn the list into a string that can be passed to a shell.
877 action_string = gyp.common.EncodePOSIXShellList(action["action"])
878
879 # Convert Xcode-type variable references to sh-compatible environment
880 # variable references.
881 message_sh = gyp.xcodeproj_file.ConvertVariablesToShellSyntax(message)
882 action_string_sh = gyp.xcodeproj_file.ConvertVariablesToShellSyntax(
883 action_string
884 )
885
886 script = ""
887 # Include the optional message
888 if message_sh:
889 script += message_sh + "\n"
890 # Be sure the script runs in exec, and that if exec fails, the script
891 # exits signalling an error.
892 script += "exec " + action_string_sh + "\nexit 1\n"
893 ssbp = gyp.xcodeproj_file.PBXShellScriptBuildPhase(
894 {
895 "inputPaths": action["inputs"],
896 "name": 'Action "' + action["action_name"] + '"',
897 "outputPaths": action["outputs"],
898 "shellScript": script,
899 "showEnvVarsInLog": 0,
900 }
901 )
902
903 if support_xct:
904 support_xct.AppendProperty("buildPhases", ssbp)
905 else:
906 # TODO(mark): this assumes too much knowledge of the internals of
907 # xcodeproj_file; some of these smarts should move into xcodeproj_file
908 # itself.
909 xct._properties["buildPhases"].insert(prebuild_index, ssbp)
910 prebuild_index = prebuild_index + 1
911
912 # TODO(mark): Should verify that at most one of these is specified.
913 if int(action.get("process_outputs_as_sources", False)):
914 for output in action["outputs"]:
915 AddSourceToTarget(output, type, pbxp, xct)
916
917 if int(action.get("process_outputs_as_mac_bundle_resources", False)):
918 for output in action["outputs"]:
919 AddResourceToTarget(output, pbxp, xct)
920
921 # tgt_mac_bundle_resources holds the list of bundle resources so
922 # the rule processing can check against it.
923 if is_bundle:
924 tgt_mac_bundle_resources = spec.get("mac_bundle_resources", [])
925 else:
926 tgt_mac_bundle_resources = []
927
928 # Add custom shell script phases driving "make" for "rules" sections.
929 #
930 # Xcode's built-in rule support is almost powerful enough to use directly,
931 # but there are a few significant deficiencies that render them unusable.
932 # There are workarounds for some of its inadequacies, but in aggregate,
933 # the workarounds added complexity to the generator, and some workarounds
934 # actually require input files to be crafted more carefully than I'd like.
935 # Consequently, until Xcode rules are made more capable, "rules" input
936 # sections will be handled in Xcode output by shell script build phases
937 # performed prior to the compilation phase.
938 #
939 # The following problems with Xcode rules were found. The numbers are
940 # Apple radar IDs. I hope that these shortcomings are addressed, I really
941 # liked having the rules handled directly in Xcode during the period that
942 # I was prototyping this.
943 #
944 # 6588600 Xcode compiles custom script rule outputs too soon, compilation
945 # fails. This occurs when rule outputs from distinct inputs are
946 # interdependent. The only workaround is to put rules and their
947 # inputs in a separate target from the one that compiles the rule
948 # outputs. This requires input file cooperation and it means that
949 # process_outputs_as_sources is unusable.
950 # 6584932 Need to declare that custom rule outputs should be excluded from
951 # compilation. A possible workaround is to lie to Xcode about a
952 # rule's output, giving it a dummy file it doesn't know how to
953 # compile. The rule action script would need to touch the dummy.
954 # 6584839 I need a way to declare additional inputs to a custom rule.
955 # A possible workaround is a shell script phase prior to
956 # compilation that touches a rule's primary input files if any
957 # would-be additional inputs are newer than the output. Modifying
958 # the source tree - even just modification times - feels dirty.
959 # 6564240 Xcode "custom script" build rules always dump all environment
960 # variables. This is a low-priority problem and is not a
961 # show-stopper.
962 rules_by_ext = {}
963 for rule in spec_rules:
964 rules_by_ext[rule["extension"]] = rule
965
966 # First, some definitions:
967 #
968 # A "rule source" is a file that was listed in a target's "sources"
969 # list and will have a rule applied to it on the basis of matching the
970 # rule's "extensions" attribute. Rule sources are direct inputs to
971 # rules.
972 #
973 # Rule definitions may specify additional inputs in their "inputs"
974 # attribute. These additional inputs are used for dependency tracking
975 # purposes.
976 #
977 # A "concrete output" is a rule output with input-dependent variables
978 # resolved. For example, given a rule with:
979 # 'extension': 'ext', 'outputs': ['$(INPUT_FILE_BASE).cc'],
980 # if the target's "sources" list contained "one.ext" and "two.ext",
981 # the "concrete output" for rule input "two.ext" would be "two.cc". If
982 # a rule specifies multiple outputs, each input file that the rule is
983 # applied to will have the same number of concrete outputs.
984 #
985 # If any concrete outputs are outdated or missing relative to their
986 # corresponding rule_source or to any specified additional input, the
987 # rule action must be performed to generate the concrete outputs.
988
989 # concrete_outputs_by_rule_source will have an item at the same index
990 # as the rule['rule_sources'] that it corresponds to. Each item is a
991 # list of all of the concrete outputs for the rule_source.
992 concrete_outputs_by_rule_source = []
993
994 # concrete_outputs_all is a flat list of all concrete outputs that this
995 # rule is able to produce, given the known set of input files
996 # (rule_sources) that apply to it.
997 concrete_outputs_all = []
998
999 # messages & actions are keyed by the same indices as rule['rule_sources']
1000 # and concrete_outputs_by_rule_source. They contain the message and
1001 # action to perform after resolving input-dependent variables. The
1002 # message is optional, in which case None is stored for each rule source.
1003 messages = []
1004 actions = []
1005
1006 for rule_source in rule.get("rule_sources", []):
1007 rule_source_dirname, rule_source_basename = posixpath.split(rule_source)
1008 (rule_source_root, rule_source_ext) = posixpath.splitext(
1009 rule_source_basename
1010 )
1011
1012 # These are the same variable names that Xcode uses for its own native
1013 # rule support. Because Xcode's rule engine is not being used, they
1014 # need to be expanded as they are written to the makefile.
1015 rule_input_dict = {
1016 "INPUT_FILE_BASE": rule_source_root,
1017 "INPUT_FILE_SUFFIX": rule_source_ext,
1018 "INPUT_FILE_NAME": rule_source_basename,
1019 "INPUT_FILE_PATH": rule_source,
1020 "INPUT_FILE_DIRNAME": rule_source_dirname,
1021 }
1022
1023 concrete_outputs_for_this_rule_source = []
1024 for output in rule.get("outputs", []):
1025 # Fortunately, Xcode and make both use $(VAR) format for their
1026 # variables, so the expansion is the only transformation necessary.
1027 # Any remaining $(VAR)-type variables in the string can be given
1028 # directly to make, which will pick up the correct settings from
1029 # what Xcode puts into the environment.
1030 concrete_output = ExpandXcodeVariables(output, rule_input_dict)
1031 concrete_outputs_for_this_rule_source.append(concrete_output)
1032
1033 # Add all concrete outputs to the project.
1034 pbxp.AddOrGetFileInRootGroup(concrete_output)
1035
1036 concrete_outputs_by_rule_source.append(
1037 concrete_outputs_for_this_rule_source
1038 )
1039 concrete_outputs_all.extend(concrete_outputs_for_this_rule_source)
1040
1041 # TODO(mark): Should verify that at most one of these is specified.
1042 if int(rule.get("process_outputs_as_sources", False)):
1043 for output in concrete_outputs_for_this_rule_source:
1044 AddSourceToTarget(output, type, pbxp, xct)
1045
1046 # If the file came from the mac_bundle_resources list or if the rule
1047 # is marked to process outputs as bundle resource, do so.
1048 was_mac_bundle_resource = rule_source in tgt_mac_bundle_resources
1049 if was_mac_bundle_resource or int(
1050 rule.get("process_outputs_as_mac_bundle_resources", False)
1051 ):
1052 for output in concrete_outputs_for_this_rule_source:
1053 AddResourceToTarget(output, pbxp, xct)
1054
1055 # Do we have a message to print when this rule runs?
1056 message = rule.get("message")
1057 if message:
1058 message = gyp.common.EncodePOSIXShellArgument(message)
1059 message = ExpandXcodeVariables(message, rule_input_dict)
1060 messages.append(message)
1061
1062 # Turn the list into a string that can be passed to a shell.
1063 action_string = gyp.common.EncodePOSIXShellList(rule["action"])
1064
1065 action = ExpandXcodeVariables(action_string, rule_input_dict)
1066 actions.append(action)
1067
1068 if len(concrete_outputs_all) > 0:
1069 # TODO(mark): There's a possibility for collision here. Consider
1070 # target "t" rule "A_r" and target "t_A" rule "r".
1071 makefile_name = "%s.make" % re.sub(
1072 "[^a-zA-Z0-9_]", "_", "{}_{}".format(target_name, rule["rule_name"])
1073 )
1074 makefile_path = os.path.join(
1075 xcode_projects[build_file].path, makefile_name
1076 )
1077 # TODO(mark): try/close? Write to a temporary file and swap it only
1078 # if it's got changes?
1079 makefile = open(makefile_path, "w")
1080
1081 # make will build the first target in the makefile by default. By
1082 # convention, it's called "all". List all (or at least one)
1083 # concrete output for each rule source as a prerequisite of the "all"
1084 # target.
1085 makefile.write("all: \\\n")
1086 for concrete_output_index, concrete_output_by_rule_source in enumerate(
1087 concrete_outputs_by_rule_source
1088 ):
1089 # Only list the first (index [0]) concrete output of each input
1090 # in the "all" target. Otherwise, a parallel make (-j > 1) would
1091 # attempt to process each input multiple times simultaneously.
1092 # Otherwise, "all" could just contain the entire list of
1093 # concrete_outputs_all.
1094 concrete_output = concrete_output_by_rule_source[0]
1095 if (
1096 concrete_output_index
1097 == len(concrete_outputs_by_rule_source) - 1
1098 ):
1099 eol = ""
1100 else:
1101 eol = " \\"
1102 makefile.write(f" {concrete_output}{eol}\n")
1103
1104 for rule_source, concrete_outputs, message, action in zip(
1105 rule["rule_sources"],
1106 concrete_outputs_by_rule_source,
1107 messages,
1108 actions,
1109 ):
1110 makefile.write("\n")
1111
1112 # Add a rule that declares it can build each concrete output of a
1113 # rule source. Collect the names of the directories that are
1114 # required.
1115 concrete_output_dirs = []
1116 for concrete_output_index, concrete_output in enumerate(
1117 concrete_outputs
1118 ):
1119 bol = "" if concrete_output_index == 0 else " "
1120 makefile.write(f"{bol}{concrete_output} \\\n")
1121
1122 concrete_output_dir = posixpath.dirname(concrete_output)
1123 if (
1124 concrete_output_dir
1125 and concrete_output_dir not in concrete_output_dirs
1126 ):
1127 concrete_output_dirs.append(concrete_output_dir)
1128
1129 makefile.write(" : \\\n")
1130
1131 # The prerequisites for this rule are the rule source itself and
1132 # the set of additional rule inputs, if any.
1133 prerequisites = [rule_source]
1134 prerequisites.extend(rule.get("inputs", []))
1135 for prerequisite_index, prerequisite in enumerate(prerequisites):
1136 if prerequisite_index == len(prerequisites) - 1:
1137 eol = ""
1138 else:
1139 eol = " \\"
1140 makefile.write(f" {prerequisite}{eol}\n")
1141
1142 # Make sure that output directories exist before executing the rule
1143 # action.
1144 if len(concrete_output_dirs) > 0:
1145 makefile.write(
1146 '\t@mkdir -p "%s"\n' % '" "'.join(concrete_output_dirs)
1147 )
1148
1149 # The rule message and action have already had
1150 # the necessary variable substitutions performed.
1151 if message:
1152 # Mark it with note: so Xcode picks it up in build output.
1153 makefile.write("\t@echo note: %s\n" % message)
1154 makefile.write("\t%s\n" % action)
1155
1156 makefile.close()
1157
1158 # It might be nice to ensure that needed output directories exist
1159 # here rather than in each target in the Makefile, but that wouldn't
1160 # work if there ever was a concrete output that had an input-dependent
1161 # variable anywhere other than in the leaf position.
1162
1163 # Don't declare any inputPaths or outputPaths. If they're present,
1164 # Xcode will provide a slight optimization by only running the script
1165 # phase if any output is missing or outdated relative to any input.
1166 # Unfortunately, it will also assume that all outputs are touched by
1167 # the script, and if the outputs serve as files in a compilation
1168 # phase, they will be unconditionally rebuilt. Since make might not
1169 # rebuild everything that could be declared here as an output, this
1170 # extra compilation activity is unnecessary. With inputPaths and
1171 # outputPaths not supplied, make will always be called, but it knows
1172 # enough to not do anything when everything is up-to-date.
1173
1174 # To help speed things up, pass -j COUNT to make so it does some work
1175 # in parallel. Don't use ncpus because Xcode will build ncpus targets
1176 # in parallel and if each target happens to have a rules step, there
1177 # would be ncpus^2 things going. With a machine that has 2 quad-core
1178 # Xeons, a build can quickly run out of processes based on
1179 # scheduling/other tasks, and randomly failing builds are no good.
1180 script = (
1181 """JOB_COUNT="$(/usr/sbin/sysctl -n hw.ncpu)"
1182if [ "${JOB_COUNT}" -gt 4 ]; then
1183 JOB_COUNT=4
1184fi
1185exec xcrun make -f "${PROJECT_FILE_PATH}/%s" -j "${JOB_COUNT}"
1186exit 1
1187"""
1188 % makefile_name
1189 )
1190 ssbp = gyp.xcodeproj_file.PBXShellScriptBuildPhase(
1191 {
1192 "name": 'Rule "' + rule["rule_name"] + '"',
1193 "shellScript": script,
1194 "showEnvVarsInLog": 0,
1195 }
1196 )
1197
1198 if support_xct:
1199 support_xct.AppendProperty("buildPhases", ssbp)
1200 else:
