| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306230723082309231023112312231323142315231623172318231923202321232223232324232523262327232823292330233123322333233423352336233723382339234023412342234323442345234623472348234923502351235223532354235523562357235823592360236123622363236423652366236723682369237023712372237323742375237623772378237923802381238223832384238523862387238823892390239123922393239423952396239723982399240024012402240324042405240624072408240924102411241224132414241524162417241824192420242124222423242424252426242724282429243024312432243324342435243624372438243924402441244224432444244524462447244824492450245124522453245424552456245724582459246024612462246324642465246624672468246924702471247224732474247524762477247824792480248124822483248424852486248724882489249024912492249324942495249624972498249925002501250225032504250525062507250825092510251125122513251425152516251725182519252025212522252325242525252625272528252925302531253225332534253525362537253825392540254125422543254425452546254725482549255025512552255325542555255625572558255925602561256225632564256525662567256825692570257125722573257425752576257725782579258025812582258325842585258625872588258925902591259225932594259525962597259825992600260126022603260426052606260726082609261026112612261326142615261626172618261926202621262226232624262526262627262826292630263126322633263426352636263726382639264026412642264326442645264626472648264926502651265226532654265526562657265826592660266126622663266426652666266726682669267026712672267326742675267626772678267926802681268226832684268526862687268826892690269126922693269426952696269726982699270027012702270327042705270627072708270927102711271227132714271527162717271827192720272127222723272427252726272727282729273027312732273327342735273627372738273927402741274227432744274527462747274827492750275127522753275427552756275727582759276027612762276327642765276627672768276927702771277227732774277527762777277827792780278127822783278427852786278727882789279027912792279327942795279627972798279928002801280228032804280528062807280828092810281128122813281428152816281728182819282028212822282328242825282628272828282928302831283228332834283528362837283828392840284128422843284428452846284728482849285028512852285328542855285628572858285928602861286228632864286528662867286828692870287128722873287428752876287728782879288028812882288328842885288628872888288928902891289228932894289528962897289828992900290129022903290429052906290729082909291029112912291329142915291629172918291929202921292229232924292529262927292829292930293129322933293429352936293729382939294029412942294329442945294629472948294929502951295229532954295529562957295829592960296129622963296429652966296729682969297029712972297329742975297629772978297929802981298229832984298529862987298829892990299129922993299429952996299729982999300030013002300330043005300630073008300930103011301230133014301530163017301830193020302130223023302430253026302730283029303030313032303330343035303630373038303930403041304230433044304530463047304830493050305130523053305430553056305730583059306030613062306330643065306630673068306930703071307230733074307530763077307830793080308130823083308430853086308730883089309030913092309330943095309630973098309931003101310231033104310531063107310831093110311131123113311431153116311731183119312031213122312331243125312631273128312931303131313231333134313531363137313831393140314131423143314431453146314731483149315031513152315331543155315631573158315931603161316231633164316531663167316831693170317131723173317431753176317731783179318031813182318331843185318631873188318931903191319231933194319531963197 |
- # Copyright (c) 2012 Google Inc. All rights reserved.
- # Use of this source code is governed by a BSD-style license that can be
- # found in the LICENSE file.
- """Xcode project file generator.
- This module is both an Xcode project file generator and a documentation of the
- Xcode project file format. Knowledge of the project file format was gained
- based on extensive experience with Xcode, and by making changes to projects in
- Xcode.app and observing the resultant changes in the associated project files.
- XCODE PROJECT FILES
- The generator targets the file format as written by Xcode 3.2 (specifically,
- 3.2.6), but past experience has taught that the format has not changed
- significantly in the past several years, and future versions of Xcode are able
- to read older project files.
- Xcode project files are "bundled": the project "file" from an end-user's
- perspective is actually a directory with an ".xcodeproj" extension. The
- project file from this module's perspective is actually a file inside this
- directory, always named "project.pbxproj". This file contains a complete
- description of the project and is all that is needed to use the xcodeproj.
- Other files contained in the xcodeproj directory are simply used to store
- per-user settings, such as the state of various UI elements in the Xcode
- application.
- The project.pbxproj file is a property list, stored in a format almost
- identical to the NeXTstep property list format. The file is able to carry
- Unicode data, and is encoded in UTF-8. The root element in the property list
- is a dictionary that contains several properties of minimal interest, and two
- properties of immense interest. The most important property is a dictionary
- named "objects". The entire structure of the project is represented by the
- children of this property. The objects dictionary is keyed by unique 96-bit
- values represented by 24 uppercase hexadecimal characters. Each value in the
- objects dictionary is itself a dictionary, describing an individual object.
- Each object in the dictionary is a member of a class, which is identified by
- the "isa" property of each object. A variety of classes are represented in a
- project file. Objects can refer to other objects by ID, using the 24-character
- hexadecimal object key. A project's objects form a tree, with a root object
- of class PBXProject at the root. As an example, the PBXProject object serves
- as parent to an XCConfigurationList object defining the build configurations
- used in the project, a PBXGroup object serving as a container for all files
- referenced in the project, and a list of target objects, each of which defines
- a target in the project. There are several different types of target object,
- such as PBXNativeTarget and PBXAggregateTarget. In this module, this
- relationship is expressed by having each target type derive from an abstract
- base named XCTarget.
- The project.pbxproj file's root dictionary also contains a property, sibling to
- the "objects" dictionary, named "rootObject". The value of rootObject is a
- 24-character object key referring to the root PBXProject object in the
- objects dictionary.
- In Xcode, every file used as input to a target or produced as a final product
- of a target must appear somewhere in the hierarchy rooted at the PBXGroup
- object referenced by the PBXProject's mainGroup property. A PBXGroup is
- generally represented as a folder in the Xcode application. PBXGroups can
- contain other PBXGroups as well as PBXFileReferences, which are pointers to
- actual files.
- Each XCTarget contains a list of build phases, represented in this module by
- the abstract base XCBuildPhase. Examples of concrete XCBuildPhase derivations
- are PBXSourcesBuildPhase and PBXFrameworksBuildPhase, which correspond to the
- "Compile Sources" and "Link Binary With Libraries" phases displayed in the
- Xcode application. Files used as input to these phases (for example, source
- files in the former case and libraries and frameworks in the latter) are
- represented by PBXBuildFile objects, referenced by elements of "files" lists
- in XCTarget objects. Each PBXBuildFile object refers to a PBXBuildFile
- object as a "weak" reference: it does not "own" the PBXBuildFile, which is
- owned by the root object's mainGroup or a descendant group. In most cases, the
- layer of indirection between an XCBuildPhase and a PBXFileReference via a
- PBXBuildFile appears extraneous, but there's actually one reason for this:
- file-specific compiler flags are added to the PBXBuildFile object so as to
- allow a single file to be a member of multiple targets while having distinct
- compiler flags for each. These flags can be modified in the Xcode applciation
- in the "Build" tab of a File Info window.
- When a project is open in the Xcode application, Xcode will rewrite it. As
- such, this module is careful to adhere to the formatting used by Xcode, to
- avoid insignificant changes appearing in the file when it is used in the
- Xcode application. This will keep version control repositories happy, and
- makes it possible to compare a project file used in Xcode to one generated by
- this module to determine if any significant changes were made in the
- application.
- Xcode has its own way of assigning 24-character identifiers to each object,
- which is not duplicated here. Because the identifier only is only generated
- once, when an object is created, and is then left unchanged, there is no need
- to attempt to duplicate Xcode's behavior in this area. The generator is free
- to select any identifier, even at random, to refer to the objects it creates,
- and Xcode will retain those identifiers and use them when subsequently
- rewriting the project file. However, the generator would choose new random
- identifiers each time the project files are generated, leading to difficulties
- comparing "used" project files to "pristine" ones produced by this module,
- and causing the appearance of changes as every object identifier is changed
- when updated projects are checked in to a version control repository. To
- mitigate this problem, this module chooses identifiers in a more deterministic
- way, by hashing a description of each object as well as its parent and ancestor
- objects. This strategy should result in minimal "shift" in IDs as successive
- generations of project files are produced.
- THIS MODULE
- This module introduces several classes, all derived from the XCObject class.
- Nearly all of the "brains" are built into the XCObject class, which understands
- how to create and modify objects, maintain the proper tree structure, compute
- identifiers, and print objects. For the most part, classes derived from
- XCObject need only provide a _schema class object, a dictionary that
- expresses what properties objects of the class may contain.
- Given this structure, it's possible to build a minimal project file by creating
- objects of the appropriate types and making the proper connections:
- config_list = XCConfigurationList()
- group = PBXGroup()
- project = PBXProject({'buildConfigurationList': config_list,
- 'mainGroup': group})
- With the project object set up, it can be added to an XCProjectFile object.
- XCProjectFile is a pseudo-class in the sense that it is a concrete XCObject
- subclass that does not actually correspond to a class type found in a project
- file. Rather, it is used to represent the project file's root dictionary.
- Printing an XCProjectFile will print the entire project file, including the
- full "objects" dictionary.
- project_file = XCProjectFile({'rootObject': project})
- project_file.ComputeIDs()
- project_file.Print()
- Xcode project files are always encoded in UTF-8. This module will accept
- strings of either the str class or the unicode class. Strings of class str
- are assumed to already be encoded in UTF-8. Obviously, if you're just using
- ASCII, you won't encounter difficulties because ASCII is a UTF-8 subset.
- Strings of class unicode are handled properly and encoded in UTF-8 when
- a project file is output.
- """
- import gyp.common
- from functools import cmp_to_key
- import hashlib
- from operator import attrgetter
- import posixpath
- import re
- import struct
- import sys
- def cmp(x, y):
- return (x > y) - (x < y)
- # See XCObject._EncodeString. This pattern is used to determine when a string
- # can be printed unquoted. Strings that match this pattern may be printed
- # unquoted. Strings that do not match must be quoted and may be further
- # transformed to be properly encoded. Note that this expression matches the
- # characters listed with "+", for 1 or more occurrences: if a string is empty,
- # it must not match this pattern, because it needs to be encoded as "".
- _unquoted = re.compile("^[A-Za-z0-9$./_]+$")
- # Strings that match this pattern are quoted regardless of what _unquoted says.
- # Oddly, Xcode will quote any string with a run of three or more underscores.
- _quoted = re.compile("___")
- # This pattern should match any character that needs to be escaped by
- # XCObject._EncodeString. See that function.
- _escaped = re.compile('[\\\\"]|[\x00-\x1f]')
- # Used by SourceTreeAndPathFromPath
- _path_leading_variable = re.compile(r"^\$\((.*?)\)(/(.*))?$")
- def SourceTreeAndPathFromPath(input_path):
- """Given input_path, returns a tuple with sourceTree and path values.
- Examples:
- input_path (source_tree, output_path)
- '$(VAR)/path' ('VAR', 'path')
- '$(VAR)' ('VAR', None)
- 'path' (None, 'path')
- """
- source_group_match = _path_leading_variable.match(input_path)
- if source_group_match:
- source_tree = source_group_match.group(1)
- output_path = source_group_match.group(3) # This may be None.
- else:
- source_tree = None
- output_path = input_path
- return (source_tree, output_path)
- def ConvertVariablesToShellSyntax(input_string):
- return re.sub(r"\$\((.*?)\)", "${\\1}", input_string)
- class XCObject:
- """The abstract base of all class types used in Xcode project files.
- Class variables:
- _schema: A dictionary defining the properties of this class. The keys to
- _schema are string property keys as used in project files. Values
- are a list of four or five elements:
- [ is_list, property_type, is_strong, is_required, default ]
- is_list: True if the property described is a list, as opposed
- to a single element.
- property_type: The type to use as the value of the property,
- or if is_list is True, the type to use for each
- element of the value's list. property_type must
- be an XCObject subclass, or one of the built-in
- types str, int, or dict.
- is_strong: If property_type is an XCObject subclass, is_strong
- is True to assert that this class "owns," or serves
- as parent, to the property value (or, if is_list is
- True, values). is_strong must be False if
- property_type is not an XCObject subclass.
- is_required: True if the property is required for the class.
- Note that is_required being True does not preclude
- an empty string ("", in the case of property_type
- str) or list ([], in the case of is_list True) from
- being set for the property.
- default: Optional. If is_required is True, default may be set
- to provide a default value for objects that do not supply
- their own value. If is_required is True and default
- is not provided, users of the class must supply their own
- value for the property.
- Note that although the values of the array are expressed in
- boolean terms, subclasses provide values as integers to conserve
- horizontal space.
- _should_print_single_line: False in XCObject. Subclasses whose objects
- should be written to the project file in the
- alternate single-line format, such as
- PBXFileReference and PBXBuildFile, should
- set this to True.
- _encode_transforms: Used by _EncodeString to encode unprintable characters.
- The index into this list is the ordinal of the
- character to transform; each value is a string
- used to represent the character in the output. XCObject
- provides an _encode_transforms list suitable for most
- XCObject subclasses.
- _alternate_encode_transforms: Provided for subclasses that wish to use
- the alternate encoding rules. Xcode seems
- to use these rules when printing objects in
- single-line format. Subclasses that desire
- this behavior should set _encode_transforms
- to _alternate_encode_transforms.
- _hashables: A list of XCObject subclasses that can be hashed by ComputeIDs
- to construct this object's ID. Most classes that need custom
- hashing behavior should do it by overriding Hashables,
- but in some cases an object's parent may wish to push a
- hashable value into its child, and it can do so by appending
- to _hashables.
- Attributes:
- id: The object's identifier, a 24-character uppercase hexadecimal string.
- Usually, objects being created should not set id until the entire
- project file structure is built. At that point, UpdateIDs() should
- be called on the root object to assign deterministic values for id to
- each object in the tree.
- parent: The object's parent. This is set by a parent XCObject when a child
- object is added to it.
- _properties: The object's property dictionary. An object's properties are
- described by its class' _schema variable.
- """
- _schema = {}
- _should_print_single_line = False
- # See _EncodeString.
- _encode_transforms = []
- i = 0
- while i < ord(" "):
- _encode_transforms.append("\\U%04x" % i)
- i = i + 1
- _encode_transforms[7] = "\\a"
- _encode_transforms[8] = "\\b"
- _encode_transforms[9] = "\\t"
- _encode_transforms[10] = "\\n"
- _encode_transforms[11] = "\\v"
- _encode_transforms[12] = "\\f"
- _encode_transforms[13] = "\\n"
- _alternate_encode_transforms = list(_encode_transforms)
- _alternate_encode_transforms[9] = chr(9)
- _alternate_encode_transforms[10] = chr(10)
- _alternate_encode_transforms[11] = chr(11)
- def __init__(self, properties=None, id=None, parent=None):
- self.id = id
- self.parent = parent
- self._properties = {}
- self._hashables = []
- self._SetDefaultsFromSchema()
- self.UpdateProperties(properties)
- def __repr__(self):
- try:
- name = self.Name()
- except NotImplementedError:
- return f"<{self.__class__.__name__} at 0x{id(self):x}>"
- return f"<{self.__class__.__name__} {name!r} at 0x{id(self):x}>"
- def Copy(self):
- """Make a copy of this object.
- The new object will have its own copy of lists and dicts. Any XCObject
- objects owned by this object (marked "strong") will be copied in the
- new object, even those found in lists. If this object has any weak
- references to other XCObjects, the same references are added to the new
- object without making a copy.
- """
- that = self.__class__(id=self.id, parent=self.parent)
- for key, value in self._properties.items():
- is_strong = self._schema[key][2]
- if isinstance(value, XCObject):
- if is_strong:
- new_value = value.Copy()
- new_value.parent = that
- that._properties[key] = new_value
- else:
- that._properties[key] = value
- elif isinstance(value, (str, int)):
- that._properties[key] = value
- elif isinstance(value, list):
- if is_strong:
- # If is_strong is True, each element is an XCObject, so it's safe to
- # call Copy.
- that._properties[key] = []
- for item in value:
- new_item = item.Copy()
- new_item.parent = that
- that._properties[key].append(new_item)
- else:
- that._properties[key] = value[:]
- elif isinstance(value, dict):
- # dicts are never strong.
- if is_strong:
- raise TypeError(
- "Strong dict for key " + key + " in " + self.__class__.__name__
- )
- else:
- that._properties[key] = value.copy()
- else:
- raise TypeError(
- "Unexpected type "
- + value.__class__.__name__
- + " for key "
- + key
- + " in "
- + self.__class__.__name__
- )
- return that
- def Name(self):
- """Return the name corresponding to an object.
- Not all objects necessarily need to be nameable, and not all that do have
- a "name" property. Override as needed.
- """
- # If the schema indicates that "name" is required, try to access the
- # property even if it doesn't exist. This will result in a KeyError
- # being raised for the property that should be present, which seems more
- # appropriate than NotImplementedError in this case.
- if "name" in self._properties or (
- "name" in self._schema and self._schema["name"][3]
- ):
- return self._properties["name"]
- raise NotImplementedError(self.__class__.__name__ + " must implement Name")
- def Comment(self):
- """Return a comment string for the object.
- Most objects just use their name as the comment, but PBXProject uses
- different values.
- The returned comment is not escaped and does not have any comment marker
- strings applied to it.
- """
- return self.Name()
- def Hashables(self):
- hashables = [self.__class__.__name__]
- name = self.Name()
- if name is not None:
- hashables.append(name)
- hashables.extend(self._hashables)
- return hashables
- def HashablesForChild(self):
- return None
- def ComputeIDs(self, recursive=True, overwrite=True, seed_hash=None):
- """Set "id" properties deterministically.
- An object's "id" property is set based on a hash of its class type and
- name, as well as the class type and name of all ancestor objects. As
- such, it is only advisable to call ComputeIDs once an entire project file
- tree is built.
- If recursive is True, recurse into all descendant objects and update their
- hashes.
- If overwrite is True, any existing value set in the "id" property will be
- replaced.
- """
- def _HashUpdate(hash, data):
- """Update hash with data's length and contents.
- If the hash were updated only with the value of data, it would be
- possible for clowns to induce collisions by manipulating the names of
- their objects. By adding the length, it's exceedingly less likely that
- ID collisions will be encountered, intentionally or not.
- """
- hash.update(struct.pack(">i", len(data)))
- if isinstance(data, str):
- data = data.encode("utf-8")
- hash.update(data)
- if seed_hash is None:
- seed_hash = hashlib.sha1()
- hash = seed_hash.copy()
- hashables = self.Hashables()
- assert len(hashables) > 0
- for hashable in hashables:
- _HashUpdate(hash, hashable)
- if recursive:
- hashables_for_child = self.HashablesForChild()
- if hashables_for_child is None:
- child_hash = hash
- else:
- assert len(hashables_for_child) > 0
- child_hash = seed_hash.copy()
- for hashable in hashables_for_child:
- _HashUpdate(child_hash, hashable)
- for child in self.Children():
- child.ComputeIDs(recursive, overwrite, child_hash)
- if overwrite or self.id is None:
- # Xcode IDs are only 96 bits (24 hex characters), but a SHA-1 digest is
- # is 160 bits. Instead of throwing out 64 bits of the digest, xor them
- # into the portion that gets used.
- assert hash.digest_size % 4 == 0
- digest_int_count = hash.digest_size // 4
- digest_ints = struct.unpack(">" + "I" * digest_int_count, hash.digest())
- id_ints = [0, 0, 0]
- for index in range(0, digest_int_count):
- id_ints[index % 3] ^= digest_ints[index]
- self.id = "%08X%08X%08X" % tuple(id_ints)
- def EnsureNoIDCollisions(self):
- """Verifies that no two objects have the same ID. Checks all descendants.
- """
- ids = {}
- descendants = self.Descendants()
- for descendant in descendants:
- if descendant.id in ids:
- other = ids[descendant.id]
- raise KeyError(
- 'Duplicate ID %s, objects "%s" and "%s" in "%s"'
- % (
- descendant.id,
- str(descendant._properties),
- str(other._properties),
- self._properties["rootObject"].Name(),
- )
- )
- ids[descendant.id] = descendant
- def Children(self):
- """Returns a list of all of this object's owned (strong) children."""
- children = []
- for property, attributes in self._schema.items():
- (is_list, property_type, is_strong) = attributes[0:3]
- if is_strong and property in self._properties:
- if not is_list:
- children.append(self._properties[property])
- else:
- children.extend(self._properties[property])
- return children
- def Descendants(self):
- """Returns a list of all of this object's descendants, including this
- object.
- """
- children = self.Children()
- descendants = [self]
- for child in children:
- descendants.extend(child.Descendants())
- return descendants
- def PBXProjectAncestor(self):
- # The base case for recursion is defined at PBXProject.PBXProjectAncestor.
- if self.parent:
- return self.parent.PBXProjectAncestor()
- return None
- def _EncodeComment(self, comment):
- """Encodes a comment to be placed in the project file output, mimicking
- Xcode behavior.
- """
- # This mimics Xcode behavior by wrapping the comment in "/*" and "*/". If
- # the string already contains a "*/", it is turned into "(*)/". This keeps
- # the file writer from outputting something that would be treated as the
- # end of a comment in the middle of something intended to be entirely a
- # comment.
- return "/* " + comment.replace("*/", "(*)/") + " */"
- def _EncodeTransform(self, match):
- # This function works closely with _EncodeString. It will only be called
- # by re.sub with match.group(0) containing a character matched by the
- # the _escaped expression.
- char = match.group(0)
- # Backslashes (\) and quotation marks (") are always replaced with a
- # backslash-escaped version of the same. Everything else gets its
- # replacement from the class' _encode_transforms array.
- if char == "\\":
- return "\\\\"
- if char == '"':
- return '\\"'
- return self._encode_transforms[ord(char)]
- def _EncodeString(self, value):
- """Encodes a string to be placed in the project file output, mimicking
- Xcode behavior.
- """
- # Use quotation marks when any character outside of the range A-Z, a-z, 0-9,
- # $ (dollar sign), . (period), and _ (underscore) is present. Also use
- # quotation marks to represent empty strings.
- #
- # Escape " (double-quote) and \ (backslash) by preceding them with a
- # backslash.
- #
- # Some characters below the printable ASCII range are encoded specially:
- # 7 ^G BEL is encoded as "\a"
- # 8 ^H BS is encoded as "\b"
- # 11 ^K VT is encoded as "\v"
- # 12 ^L NP is encoded as "\f"
- # 127 ^? DEL is passed through as-is without escaping
- # - In PBXFileReference and PBXBuildFile objects:
- # 9 ^I HT is passed through as-is without escaping
- # 10 ^J NL is passed through as-is without escaping
- # 13 ^M CR is passed through as-is without escaping
- # - In other objects:
- # 9 ^I HT is encoded as "\t"
- # 10 ^J NL is encoded as "\n"
- # 13 ^M CR is encoded as "\n" rendering it indistinguishable from
- # 10 ^J NL
- # All other characters within the ASCII control character range (0 through
- # 31 inclusive) are encoded as "\U001f" referring to the Unicode code point
- # in hexadecimal. For example, character 14 (^N SO) is encoded as "\U000e".
- # Characters above the ASCII range are passed through to the output encoded
- # as UTF-8 without any escaping. These mappings are contained in the
- # class' _encode_transforms list.
- if _unquoted.search(value) and not _quoted.search(value):
- return value
- return '"' + _escaped.sub(self._EncodeTransform, value) + '"'
- def _XCPrint(self, file, tabs, line):
- file.write("\t" * tabs + line)
- def _XCPrintableValue(self, tabs, value, flatten_list=False):
- """Returns a representation of value that may be printed in a project file,
- mimicking Xcode's behavior.
- _XCPrintableValue can handle str and int values, XCObjects (which are
- made printable by returning their id property), and list and dict objects
- composed of any of the above types. When printing a list or dict, and
- _should_print_single_line is False, the tabs parameter is used to determine
- how much to indent the lines corresponding to the items in the list or
- dict.
- If flatten_list is True, single-element lists will be transformed into
- strings.
- """
- printable = ""
- comment = None
- if self._should_print_single_line:
- sep = " "
- element_tabs = ""
- end_tabs = ""
- else:
- sep = "\n"
- element_tabs = "\t" * (tabs + 1)
- end_tabs = "\t" * tabs
- if isinstance(value, XCObject):
- printable += value.id
- comment = value.Comment()
- elif isinstance(value, str):
- printable += self._EncodeString(value)
- elif isinstance(value, str):
- printable += self._EncodeString(value.encode("utf-8"))
- elif isinstance(value, int):
- printable += str(value)
- elif isinstance(value, list):
- if flatten_list and len(value) <= 1:
- if len(value) == 0:
- printable += self._EncodeString("")
- else:
- printable += self._EncodeString(value[0])
- else:
- printable = "(" + sep
- for item in value:
- printable += (
- element_tabs
- + self._XCPrintableValue(tabs + 1, item, flatten_list)
- + ","
- + sep
- )
- printable += end_tabs + ")"
- elif isinstance(value, dict):
- printable = "{" + sep
- for item_key, item_value in sorted(value.items()):
- printable += (
- element_tabs
- + self._XCPrintableValue(tabs + 1, item_key, flatten_list)
- + " = "
- + self._XCPrintableValue(tabs + 1, item_value, flatten_list)
- + ";"
- + sep
- )
- printable += end_tabs + "}"
- else:
- raise TypeError("Can't make " + value.__class__.__name__ + " printable")
- if comment:
- printable += " " + self._EncodeComment(comment)
- return printable
- def _XCKVPrint(self, file, tabs, key, value):
- """Prints a key and value, members of an XCObject's _properties dictionary,
- to file.
- tabs is an int identifying the indentation level. If the class'
- _should_print_single_line variable is True, tabs is ignored and the
- key-value pair will be followed by a space insead of a newline.
- """
- if self._should_print_single_line:
- printable = ""
- after_kv = " "
- else:
- printable = "\t" * tabs
- after_kv = "\n"
- # Xcode usually prints remoteGlobalIDString values in PBXContainerItemProxy
- # objects without comments. Sometimes it prints them with comments, but
- # the majority of the time, it doesn't. To avoid unnecessary changes to
- # the project file after Xcode opens it, don't write comments for
- # remoteGlobalIDString. This is a sucky hack and it would certainly be
- # cleaner to extend the schema to indicate whether or not a comment should
- # be printed, but since this is the only case where the problem occurs and
- # Xcode itself can't seem to make up its mind, the hack will suffice.
- #
- # Also see PBXContainerItemProxy._schema['remoteGlobalIDString'].
- if key == "remoteGlobalIDString" and isinstance(self, PBXContainerItemProxy):
- value_to_print = value.id
- else:
- value_to_print = value
- # PBXBuildFile's settings property is represented in the output as a dict,
- # but a hack here has it represented as a string. Arrange to strip off the
- # quotes so that it shows up in the output as expected.
- if key == "settings" and isinstance(self, PBXBuildFile):
- strip_value_quotes = True
- else:
- strip_value_quotes = False
- # In another one-off, let's set flatten_list on buildSettings properties
- # of XCBuildConfiguration objects, because that's how Xcode treats them.
- if key == "buildSettings" and isinstance(self, XCBuildConfiguration):
- flatten_list = True
- else:
- flatten_list = False
- try:
- printable_key = self._XCPrintableValue(tabs, key, flatten_list)
- printable_value = self._XCPrintableValue(tabs, value_to_print, flatten_list)
- if (
- strip_value_quotes
- and len(printable_value) > 1
- and printable_value[0] == '"'
- and printable_value[-1] == '"'
- ):
- printable_value = printable_value[1:-1]
- printable += printable_key + " = " + printable_value + ";" + after_kv
- except TypeError as e:
- gyp.common.ExceptionAppend(e, 'while printing key "%s"' % key)
- raise
- self._XCPrint(file, 0, printable)
- def Print(self, file=sys.stdout):
- """Prints a reprentation of this object to file, adhering to Xcode output
- formatting.
- """
- self.VerifyHasRequiredProperties()
- if self._should_print_single_line:
- # When printing an object in a single line, Xcode doesn't put any space
- # between the beginning of a dictionary (or presumably a list) and the
- # first contained item, so you wind up with snippets like
- # ...CDEF = {isa = PBXFileReference; fileRef = 0123...
- # If it were me, I would have put a space in there after the opening
- # curly, but I guess this is just another one of those inconsistencies
- # between how Xcode prints PBXFileReference and PBXBuildFile objects as
- # compared to other objects. Mimic Xcode's behavior here by using an
- # empty string for sep.
- sep = ""
- end_tabs = 0
- else:
- sep = "\n"
- end_tabs = 2
- # Start the object. For example, '\t\tPBXProject = {\n'.
- self._XCPrint(file, 2, self._XCPrintableValue(2, self) + " = {" + sep)
- # "isa" isn't in the _properties dictionary, it's an intrinsic property
- # of the class which the object belongs to. Xcode always outputs "isa"
- # as the first element of an object dictionary.
- self._XCKVPrint(file, 3, "isa", self.__class__.__name__)
- # The remaining elements of an object dictionary are sorted alphabetically.
- for property, value in sorted(self._properties.items()):
- self._XCKVPrint(file, 3, property, value)
- # End the object.
- self._XCPrint(file, end_tabs, "};\n")
- def UpdateProperties(self, properties, do_copy=False):
- """Merge the supplied properties into the _properties dictionary.
- The input properties must adhere to the class schema or a KeyError or
- TypeError exception will be raised. If adding an object of an XCObject
- subclass and the schema indicates a strong relationship, the object's
- parent will be set to this object.
- If do_copy is True, then lists, dicts, strong-owned XCObjects, and
- strong-owned XCObjects in lists will be copied instead of having their
- references added.
- """
- if properties is None:
- return
- for property, value in properties.items():
- # Make sure the property is in the schema.
- if property not in self._schema:
- raise KeyError(property + " not in " + self.__class__.__name__)
- # Make sure the property conforms to the schema.
- (is_list, property_type, is_strong) = self._schema[property][0:3]
- if is_list:
- if value.__class__ != list:
- raise TypeError(
- property
- + " of "
- + self.__class__.__name__
- + " must be list, not "
- + value.__class__.__name__
- )
- for item in value:
- if not isinstance(item, property_type) and not (
- isinstance(item, str) and property_type == str
- ):
- # Accept unicode where str is specified. str is treated as
- # UTF-8-encoded.
- raise TypeError(
- "item of "
- + property
- + " of "
- + self.__class__.__name__
- + " must be "
- + property_type.__name__
- + ", not "
- + item.__class__.__name__
- )
- elif not isinstance(value, property_type) and not (
- isinstance(value, str) and property_type == str
- ):
- # Accept unicode where str is specified. str is treated as
- # UTF-8-encoded.
- raise TypeError(
- property
- + " of "
- + self.__class__.__name__
- + " must be "
- + property_type.__name__
- + ", not "
- + value.__class__.__name__
- )
- # Checks passed, perform the assignment.
- if do_copy:
- if isinstance(value, XCObject):
- if is_strong:
- self._properties[property] = value.Copy()
- else:
- self._properties[property] = value
- elif isinstance(value, (str, int)):
- self._properties[property] = value
- elif isinstance(value, list):
- if is_strong:
- # If is_strong is True, each element is an XCObject,
- # so it's safe to call Copy.
- self._properties[property] = []
- for item in value:
- self._properties[property].append(item.Copy())
- else:
- self._properties[property] = value[:]
- elif isinstance(value, dict):
- self._properties[property] = value.copy()
- else:
- raise TypeError(
- "Don't know how to copy a "
- + value.__class__.__name__
- + " object for "
- + property
- + " in "
- + self.__class__.__name__
- )
- else:
- self._properties[property] = value
- # Set up the child's back-reference to this object. Don't use |value|
- # any more because it may not be right if do_copy is true.
- if is_strong:
- if not is_list:
- self._properties[property].parent = self
- else:
- for item in self._properties[property]:
- item.parent = self
- def HasProperty(self, key):
- return key in self._properties
- def GetProperty(self, key):
- return self._properties[key]
- def SetProperty(self, key, value):
- self.UpdateProperties({key: value})
- def DelProperty(self, key):
- if key in self._properties:
- del self._properties[key]
- def AppendProperty(self, key, value):
- # TODO(mark): Support ExtendProperty too (and make this call that)?
- # Schema validation.
- if key not in self._schema:
- raise KeyError(key + " not in " + self.__class__.__name__)
- (is_list, property_type, is_strong) = self._schema[key][0:3]
- if not is_list:
- raise TypeError(key + " of " + self.__class__.__name__ + " must be list")
- if not isinstance(value, property_type):
- raise TypeError(
- "item of "
- + key
- + " of "
- + self.__class__.__name__
- + " must be "
- + property_type.__name__
- + ", not "
- + value.__class__.__name__
- )
- # If the property doesn't exist yet, create a new empty list to receive the
- # item.
- self._properties[key] = self._properties.get(key, [])
- # Set up the ownership link.
- if is_strong:
- value.parent = self
- # Store the item.
- self._properties[key].append(value)
- def VerifyHasRequiredProperties(self):
- """Ensure that all properties identified as required by the schema are
- set.
- """
- # TODO(mark): A stronger verification mechanism is needed. Some
- # subclasses need to perform validation beyond what the schema can enforce.
- for property, attributes in self._schema.items():
- (is_list, property_type, is_strong, is_required) = attributes[0:4]
- if is_required and property not in self._properties:
- raise KeyError(self.__class__.__name__ + " requires " + property)
- def _SetDefaultsFromSchema(self):
- """Assign object default values according to the schema. This will not
- overwrite properties that have already been set."""
- defaults = {}
- for property, attributes in self._schema.items():
- (is_list, property_type, is_strong, is_required) = attributes[0:4]
- if (
- is_required
- and len(attributes) >= 5
- and property not in self._properties
- ):
- default = attributes[4]
- defaults[property] = default
- if len(defaults) > 0:
- # Use do_copy=True so that each new object gets its own copy of strong
- # objects, lists, and dicts.
- self.UpdateProperties(defaults, do_copy=True)
- class XCHierarchicalElement(XCObject):
- """Abstract base for PBXGroup and PBXFileReference. Not represented in a
- project file."""
- # TODO(mark): Do name and path belong here? Probably so.
- # If path is set and name is not, name may have a default value. Name will
- # be set to the basename of path, if the basename of path is different from
- # the full value of path. If path is already just a leaf name, name will
- # not be set.
- _schema = XCObject._schema.copy()
- _schema.update(
- {
- "comments": [0, str, 0, 0],
- "fileEncoding": [0, str, 0, 0],
- "includeInIndex": [0, int, 0, 0],
- "indentWidth": [0, int, 0, 0],
- "lineEnding": [0, int, 0, 0],
- "sourceTree": [0, str, 0, 1, "<group>"],
- "tabWidth": [0, int, 0, 0],
- "usesTabs": [0, int, 0, 0],
- "wrapsLines": [0, int, 0, 0],
- }
- )
- def __init__(self, properties=None, id=None, parent=None):
- # super
- XCObject.__init__(self, properties, id, parent)
- if "path" in self._properties and "name" not in self._properties:
- path = self._properties["path"]
- name = posixpath.basename(path)
- if name != "" and path != name:
- self.SetProperty("name", name)
- if "path" in self._properties and (
- "sourceTree" not in self._properties
- or self._properties["sourceTree"] == "<group>"
- ):
- # If the pathname begins with an Xcode variable like "$(SDKROOT)/", take
- # the variable out and make the path be relative to that variable by
- # assigning the variable name as the sourceTree.
- (source_tree, path) = SourceTreeAndPathFromPath(self._properties["path"])
- if source_tree is not None:
- self._properties["sourceTree"] = source_tree
- if path is not None:
- self._properties["path"] = path
- if (
- source_tree is not None
- and path is None
- and "name" not in self._properties
- ):
- # The path was of the form "$(SDKROOT)" with no path following it.
- # This object is now relative to that variable, so it has no path
- # attribute of its own. It does, however, keep a name.
- del self._properties["path"]
- self._properties["name"] = source_tree
- def Name(self):
- if "name" in self._properties:
- return self._properties["name"]
- elif "path" in self._properties:
- return self._properties["path"]
- else:
- # This happens in the case of the root PBXGroup.
- return None
- def Hashables(self):
- """Custom hashables for XCHierarchicalElements.
- XCHierarchicalElements are special. Generally, their hashes shouldn't
- change if the paths don't change. The normal XCObject implementation of
- Hashables adds a hashable for each object, which means that if
- the hierarchical structure changes (possibly due to changes caused when
- TakeOverOnlyChild runs and encounters slight changes in the hierarchy),
- the hashes will change. For example, if a project file initially contains
- a/b/f1 and a/b becomes collapsed into a/b, f1 will have a single parent
- a/b. If someone later adds a/f2 to the project file, a/b can no longer be
- collapsed, and f1 winds up with parent b and grandparent a. That would
- be sufficient to change f1's hash.
- To counteract this problem, hashables for all XCHierarchicalElements except
- for the main group (which has neither a name nor a path) are taken to be
- just the set of path components. Because hashables are inherited from
- parents, this provides assurance that a/b/f1 has the same set of hashables
- whether its parent is b or a/b.
- The main group is a special case. As it is permitted to have no name or
- path, it is permitted to use the standard XCObject hash mechanism. This
- is not considered a problem because there can be only one main group.
- """
- if self == self.PBXProjectAncestor()._properties["mainGroup"]:
- # super
- return XCObject.Hashables(self)
- hashables = []
- # Put the name in first, ensuring that if TakeOverOnlyChild collapses
- # children into a top-level group like "Source", the name always goes
- # into the list of hashables without interfering with path components.
- if "name" in self._properties:
- # Make it less likely for people to manipulate hashes by following the
- # pattern of always pushing an object type value onto the list first.
- hashables.append(self.__class__.__name__ + ".name")
- hashables.append(self._properties["name"])
- # NOTE: This still has the problem that if an absolute path is encountered,
- # including paths with a sourceTree, they'll still inherit their parents'
- # hashables, even though the paths aren't relative to their parents. This
- # is not expected to be much of a problem in practice.
- path = self.PathFromSourceTreeAndPath()
- if path is not None:
- components = path.split(posixpath.sep)
- for component in components:
- hashables.append(self.__class__.__name__ + ".path")
- hashables.append(component)
- hashables.extend(self._hashables)
- return hashables
- def Compare(self, other):
- # Allow comparison of these types. PBXGroup has the highest sort rank;
- # PBXVariantGroup is treated as equal to PBXFileReference.
- valid_class_types = {
- PBXFileReference: "file",
- PBXGroup: "group",
- PBXVariantGroup: "file",
- }
- self_type = valid_class_types[self.__class__]
- other_type = valid_class_types[other.__class__]
- if self_type == other_type:
- # If the two objects are of the same sort rank, compare their names.
- return cmp(self.Name(), other.Name())
- # Otherwise, sort groups before everything else.
- if self_type == "group":
- return -1
- return 1
- def CompareRootGroup(self, other):
- # This function should be used only to compare direct children of the
- # containing PBXProject's mainGroup. These groups should appear in the
- # listed order.
- # TODO(mark): "Build" is used by gyp.generator.xcode, perhaps the
- # generator should have a way of influencing this list rather than having
- # to hardcode for the generator here.
- order = [
- "Source",
- "Intermediates",
- "Projects",
- "Frameworks",
- "Products",
- "Build",
- ]
- # If the groups aren't in the listed order, do a name comparison.
- # Otherwise, groups in the listed order should come before those that
- # aren't.
- self_name = self.Name()
- other_name = other.Name()
- self_in = isinstance(self, PBXGroup) and self_name in order
- other_in = isinstance(self, PBXGroup) and other_name in order
- if not self_in and not other_in:
- return self.Compare(other)
- if self_name in order and other_name not in order:
- return -1
- if other_name in order and self_name not in order:
- return 1
- # If both groups are in the listed order, go by the defined order.
- self_index = order.index(self_name)
- other_index = order.index(other_name)
- if self_index < other_index:
- return -1
- if self_index > other_index:
- return 1
- return 0
- def PathFromSourceTreeAndPath(self):
- # Turn the object's sourceTree and path properties into a single flat
- # string of a form comparable to the path parameter. If there's a
- # sourceTree property other than "<group>", wrap it in $(...) for the
- # comparison.
- components = []
- if self._properties["sourceTree"] != "<group>":
- components.append("$(" + self._properties["sourceTree"] + ")")
- if "path" in self._properties:
- components.append(self._properties["path"])
- if len(components) > 0:
- return posixpath.join(*components)
- return None
- def FullPath(self):
- # Returns a full path to self relative to the project file, or relative
- # to some other source tree. Start with self, and walk up the chain of
- # parents prepending their paths, if any, until no more parents are
- # available (project-relative path) or until a path relative to some
- # source tree is found.
- xche = self
- path = None
- while isinstance(xche, XCHierarchicalElement) and (
- path is None or (not path.startswith("/") and not path.startswith("$"))
- ):
- this_path = xche.PathFromSourceTreeAndPath()
- if this_path is not None and path is not None:
- path = posixpath.join(this_path, path)
- elif this_path is not None:
- path = this_path
- xche = xche.parent
- return path
- class PBXGroup(XCHierarchicalElement):
- """
- Attributes:
- _children_by_path: Maps pathnames of children of this PBXGroup to the
- actual child XCHierarchicalElement objects.
- _variant_children_by_name_and_path: Maps (name, path) tuples of
- PBXVariantGroup children to the actual child PBXVariantGroup objects.
- """
- _schema = XCHierarchicalElement._schema.copy()
- _schema.update(
- {
- "children": [1, XCHierarchicalElement, 1, 1, []],
- "name": [0, str, 0, 0],
- "path": [0, str, 0, 0],
- }
- )
- def __init__(self, properties=None, id=None, parent=None):
- # super
- XCHierarchicalElement.__init__(self, properties, id, parent)
- self._children_by_path = {}
- self._variant_children_by_name_and_path = {}
- for child in self._properties.get("children", []):
- self._AddChildToDicts(child)
- def Hashables(self):
- # super
- hashables = XCHierarchicalElement.Hashables(self)
- # It is not sufficient to just rely on name and parent to build a unique
- # hashable : a node could have two child PBXGroup sharing a common name.
- # To add entropy the hashable is enhanced with the names of all its
- # children.
- for child in self._properties.get("children", []):
- child_name = child.Name()
- if child_name is not None:
- hashables.append(child_name)
- return hashables
- def HashablesForChild(self):
- # To avoid a circular reference the hashables used to compute a child id do
- # not include the child names.
- return XCHierarchicalElement.Hashables(self)
- def _AddChildToDicts(self, child):
- # Sets up this PBXGroup object's dicts to reference the child properly.
- child_path = child.PathFromSourceTreeAndPath()
- if child_path:
- if child_path in self._children_by_path:
- raise ValueError("Found multiple children with path " + child_path)
- self._children_by_path[child_path] = child
- if isinstance(child, PBXVariantGroup):
- child_name = child._properties.get("name", None)
- key = (child_name, child_path)
- if key in self._variant_children_by_name_and_path:
- raise ValueError(
- "Found multiple PBXVariantGroup children with "
- + "name "
- + str(child_name)
- + " and path "
- + str(child_path)
- )
- self._variant_children_by_name_and_path[key] = child
- def AppendChild(self, child):
- # Callers should use this instead of calling
- # AppendProperty('children', child) directly because this function
- # maintains the group's dicts.
- self.AppendProperty("children", child)
- self._AddChildToDicts(child)
- def GetChildByName(self, name):
- # This is not currently optimized with a dict as GetChildByPath is because
- # it has few callers. Most callers probably want GetChildByPath. This
- # function is only useful to get children that have names but no paths,
- # which is rare. The children of the main group ("Source", "Products",
- # etc.) is pretty much the only case where this likely to come up.
- #
- # TODO(mark): Maybe this should raise an error if more than one child is
- # present with the same name.
- if "children" not in self._properties:
- return None
- for child in self._properties["children"]:
- if child.Name() == name:
- return child
- return None
- def GetChildByPath(self, path):
- if not path:
- return None
- if path in self._children_by_path:
- return self._children_by_path[path]
- return None
- def GetChildByRemoteObject(self, remote_object):
- # This method is a little bit esoteric. Given a remote_object, which
- # should be a PBXFileReference in another project file, this method will
- # return this group's PBXReferenceProxy object serving as a local proxy
- # for the remote PBXFileReference.
- #
- # This function might benefit from a dict optimization as GetChildByPath
- # for some workloads, but profiling shows that it's not currently a
- # problem.
- if "children" not in self._properties:
- return None
- for child in self._properties["children"]:
- if not isinstance(child, PBXReferenceProxy):
- continue
- container_proxy = child._properties["remoteRef"]
- if container_proxy._properties["remoteGlobalIDString"] == remote_object:
- return child
- return None
- def AddOrGetFileByPath(self, path, hierarchical):
- """Returns an existing or new file reference corresponding to path.
- If hierarchical is True, this method will create or use the necessary
- hierarchical group structure corresponding to path. Otherwise, it will
- look in and create an item in the current group only.
- If an existing matching reference is found, it is returned, otherwise, a
- new one will be created, added to the correct group, and returned.
- If path identifies a directory by virtue of carrying a trailing slash,
- this method returns a PBXFileReference of "folder" type. If path
- identifies a variant, by virtue of it identifying a file inside a directory
- with an ".lproj" extension, this method returns a PBXVariantGroup
- containing the variant named by path, and possibly other variants. For
- all other paths, a "normal" PBXFileReference will be returned.
- """
- # Adding or getting a directory? Directories end with a trailing slash.
- is_dir = False
- if path.endswith("/"):
- is_dir = True
- path = posixpath.normpath(path)
- if is_dir:
- path = path + "/"
- # Adding or getting a variant? Variants are files inside directories
- # with an ".lproj" extension. Xcode uses variants for localization. For
- # a variant path/to/Language.lproj/MainMenu.nib, put a variant group named
- # MainMenu.nib inside path/to, and give it a variant named Language. In
- # this example, grandparent would be set to path/to and parent_root would
- # be set to Language.
- variant_name = None
- parent = posixpath.dirname(path)
- grandparent = posixpath.dirname(parent)
- parent_basename = posixpath.basename(parent)
- (parent_root, parent_ext) = posixpath.splitext(parent_basename)
- if parent_ext == ".lproj":
- variant_name = parent_root
- if grandparent == "":
- grandparent = None
- # Putting a directory inside a variant group is not currently supported.
- assert not is_dir or variant_name is None
- path_split = path.split(posixpath.sep)
- if (
- len(path_split) == 1
- or ((is_dir or variant_name is not None) and len(path_split) == 2)
- or not hierarchical
- ):
- # The PBXFileReference or PBXVariantGroup will be added to or gotten from
- # this PBXGroup, no recursion necessary.
- if variant_name is None:
- # Add or get a PBXFileReference.
- file_ref = self.GetChildByPath(path)
- if file_ref is not None:
- assert file_ref.__class__ == PBXFileReference
- else:
- file_ref = PBXFileReference({"path": path})
- self.AppendChild(file_ref)
- else:
- # Add or get a PBXVariantGroup. The variant group name is the same
- # as the basename (MainMenu.nib in the example above). grandparent
- # specifies the path to the variant group itself, and path_split[-2:]
- # is the path of the specific variant relative to its group.
- variant_group_name = posixpath.basename(path)
- variant_group_ref = self.AddOrGetVariantGroupByNameAndPath(
- variant_group_name, grandparent
- )
- variant_path = posixpath.sep.join(path_split[-2:])
- variant_ref = variant_group_ref.GetChildByPath(variant_path)
- if variant_ref is not None:
- assert variant_ref.__class__ == PBXFileReference
- else:
- variant_ref = PBXFileReference(
- {"name": variant_name, "path": variant_path}
- )
- variant_group_ref.AppendChild(variant_ref)
- # The caller is interested in the variant group, not the specific
- # variant file.
- file_ref = variant_group_ref
- return file_ref
- else:
- # Hierarchical recursion. Add or get a PBXGroup corresponding to the
- # outermost path component, and then recurse into it, chopping off that
- # path component.
- next_dir = path_split[0]
- group_ref = self.GetChildByPath(next_dir)
- if group_ref is not None:
- assert group_ref.__class__ == PBXGroup
- else:
- group_ref = PBXGroup({"path": next_dir})
- self.AppendChild(group_ref)
- return group_ref.AddOrGetFileByPath(
- posixpath.sep.join(path_split[1:]), hierarchical
- )
- def AddOrGetVariantGroupByNameAndPath(self, name, path):
- """Returns an existing or new PBXVariantGroup for name and path.
- If a PBXVariantGroup identified by the name and path arguments is already
- present as a child of this object, it is returned. Otherwise, a new
- PBXVariantGroup with the correct properties is created, added as a child,
- and returned.
- This method will generally be called by AddOrGetFileByPath, which knows
- when to create a variant group based on the structure of the pathnames
- passed to it.
- """
- key = (name, path)
- if key in self._variant_children_by_name_and_path:
- variant_group_ref = self._variant_children_by_name_and_path[key]
- assert variant_group_ref.__class__ == PBXVariantGroup
- return variant_group_ref
- variant_group_properties = {"name": name}
- if path is not None:
- variant_group_properties["path"] = path
- variant_group_ref = PBXVariantGroup(variant_group_properties)
- self.AppendChild(variant_group_ref)
- return variant_group_ref
- def TakeOverOnlyChild(self, recurse=False):
- """If this PBXGroup has only one child and it's also a PBXGroup, take
- it over by making all of its children this object's children.
- This function will continue to take over only children when those children
- are groups. If there are three PBXGroups representing a, b, and c, with
- c inside b and b inside a, and a and b have no other children, this will
- result in a taking over both b and c, forming a PBXGroup for a/b/c.
- If recurse is True, this function will recurse into children and ask them
- to collapse themselves by taking over only children as well. Assuming
- an example hierarchy with files at a/b/c/d1, a/b/c/d2, and a/b/c/d3/e/f
- (d1, d2, and f are files, the rest are groups), recursion will result in
- a group for a/b/c containing a group for d3/e.
- """
- # At this stage, check that child class types are PBXGroup exactly,
- # instead of using isinstance. The only subclass of PBXGroup,
- # PBXVariantGroup, should not participate in reparenting in the same way:
- # reparenting by merging different object types would be wrong.
- while (
- len(self._properties["children"]) == 1
- and self._properties["children"][0].__class__ == PBXGroup
- ):
- # Loop to take over the innermost only-child group possible.
- child = self._properties["children"][0]
- # Assume the child's properties, including its children. Save a copy
- # of this object's old properties, because they'll still be needed.
- # This object retains its existing id and parent attributes.
- old_properties = self._properties
- self._properties = child._properties
- self._children_by_path = child._children_by_path
- if (
- "sourceTree" not in self._properties
- or self._properties["sourceTree"] == "<group>"
- ):
- # The child was relative to its parent. Fix up the path. Note that
- # children with a sourceTree other than "<group>" are not relative to
- # their parents, so no path fix-up is needed in that case.
- if "path" in old_properties:
- if "path" in self._properties:
- # Both the original parent and child have paths set.
- self._properties["path"] = posixpath.join(
- old_properties["path"], self._properties["path"]
- )
- else:
- # Only the original parent has a path, use it.
- self._properties["path"] = old_properties["path"]
- if "sourceTree" in old_properties:
- # The original parent had a sourceTree set, use it.
- self._properties["sourceTree"] = old_properties["sourceTree"]
- # If the original parent had a name set, keep using it. If the original
- # parent didn't have a name but the child did, let the child's name
- # live on. If the name attribute seems unnecessary now, get rid of it.
- if "name" in old_properties and old_properties["name"] not in (
- None,
- self.Name(),
- ):
- self._properties["name"] = old_properties["name"]
- if (
- "name" in self._properties
- and "path" in self._properties
- and self._properties["name"] == self._properties["path"]
- ):
- del self._properties["name"]
- # Notify all children of their new parent.
- for child in self._properties["children"]:
- child.parent = self
- # If asked to recurse, recurse.
- if recurse:
- for child in self._properties["children"]:
- if child.__class__ == PBXGroup:
- child.TakeOverOnlyChild(recurse)
- def SortGroup(self):
- self._properties["children"] = sorted(
- self._properties["children"], key=cmp_to_key(lambda x, y: x.Compare(y))
- )
- # Recurse.
- for child in self._properties["children"]:
- if isinstance(child, PBXGroup):
- child.SortGroup()
- class XCFileLikeElement(XCHierarchicalElement):
- # Abstract base for objects that can be used as the fileRef property of
- # PBXBuildFile.
- def PathHashables(self):
- # A PBXBuildFile that refers to this object will call this method to
- # obtain additional hashables specific to this XCFileLikeElement. Don't
- # just use this object's hashables, they're not specific and unique enough
- # on their own (without access to the parent hashables.) Instead, provide
- # hashables that identify this object by path by getting its hashables as
- # well as the hashables of ancestor XCHierarchicalElement objects.
- hashables = []
- xche = self
- while isinstance(xche, XCHierarchicalElement):
- xche_hashables = xche.Hashables()
- for index, xche_hashable in enumerate(xche_hashables):
- hashables.insert(index, xche_hashable)
- xche = xche.parent
- return hashables
- class XCContainerPortal(XCObject):
- # Abstract base for objects that can be used as the containerPortal property
- # of PBXContainerItemProxy.
- pass
- class XCRemoteObject(XCObject):
- # Abstract base for objects that can be used as the remoteGlobalIDString
- # property of PBXContainerItemProxy.
- pass
- class PBXFileReference(XCFileLikeElement, XCContainerPortal, XCRemoteObject):
- _schema = XCFileLikeElement._schema.copy()
- _schema.update(
- {
- "explicitFileType": [0, str, 0, 0],
- "lastKnownFileType": [0, str, 0, 0],
- "name": [0, str, 0, 0],
- "path": [0, str, 0, 1],
- }
- )
- # Weird output rules for PBXFileReference.
- _should_print_single_line = True
- # super
- _encode_transforms = XCFileLikeElement._alternate_encode_transforms
- def __init__(self, properties=None, id=None, parent=None):
- # super
- XCFileLikeElement.__init__(self, properties, id, parent)
- if "path" in self._properties and self._properties["path"].endswith("/"):
- self._properties["path"] = self._properties["path"][:-1]
- is_dir = True
- else:
- is_dir = False
- if (
- "path" in self._properties
- and "lastKnownFileType" not in self._properties
- and "explicitFileType" not in self._properties
- ):
- # TODO(mark): This is the replacement for a replacement for a quick hack.
- # It is no longer incredibly sucky, but this list needs to be extended.
- extension_map = {
- "a": "archive.ar",
- "app": "wrapper.application",
- "bdic": "file",
- "bundle": "wrapper.cfbundle",
- "c": "sourcecode.c.c",
- "cc": "sourcecode.cpp.cpp",
- "cpp": "sourcecode.cpp.cpp",
- "css": "text.css",
- "cxx": "sourcecode.cpp.cpp",
- "dart": "sourcecode",
- "dylib": "compiled.mach-o.dylib",
- "framework": "wrapper.framework",
- "gyp": "sourcecode",
- "gypi": "sourcecode",
- "h": "sourcecode.c.h",
- "hxx": "sourcecode.cpp.h",
- "icns": "image.icns",
- "java": "sourcecode.java",
- "js": "sourcecode.javascript",
- "kext": "wrapper.kext",
- "m": "sourcecode.c.objc",
- "mm": "sourcecode.cpp.objcpp",
- "nib": "wrapper.nib",
- "o": "compiled.mach-o.objfile",
- "pdf": "image.pdf",
- "pl": "text.script.perl",
- "plist": "text.plist.xml",
- "pm": "text.script.perl",
- "png": "image.png",
- "py": "text.script.python",
- "r": "sourcecode.rez",
- "rez": "sourcecode.rez",
- "s": "sourcecode.asm",
- "storyboard": "file.storyboard",
- "strings": "text.plist.strings",
- "swift": "sourcecode.swift",
- "ttf": "file",
- "xcassets": "folder.assetcatalog",
- "xcconfig": "text.xcconfig",
- "xcdatamodel": "wrapper.xcdatamodel",
- "xcdatamodeld": "wrapper.xcdatamodeld",
- "xib": "file.xib",
- "y": "sourcecode.yacc",
- }
- prop_map = {
- "dart": "explicitFileType",
- "gyp": "explicitFileType",
- "gypi": "explicitFileType",
- }
- if is_dir:
- file_type = "folder"
- prop_name = "lastKnownFileType"
- else:
- basename = posixpath.basename(self._properties["path"])
- (root, ext) = posixpath.splitext(basename)
- # Check the map using a lowercase extension.
- # TODO(mark): Maybe it should try with the original case first and fall
- # back to lowercase, in case there are any instances where case
- # matters. There currently aren't.
- if ext != "":
- ext = ext[1:].lower()
- # TODO(mark): "text" is the default value, but "file" is appropriate
- # for unrecognized files not containing text. Xcode seems to choose
- # based on content.
- file_type = extension_map.get(ext, "text")
- prop_name = prop_map.get(ext, "lastKnownFileType")
- self._properties[prop_name] = file_type
- class PBXVariantGroup(PBXGroup, XCFileLikeElement):
- """PBXVariantGroup is used by Xcode to represent localizations."""
- # No additions to the schema relative to PBXGroup.
- pass
- # PBXReferenceProxy is also an XCFileLikeElement subclass. It is defined below
- # because it uses PBXContainerItemProxy, defined below.
- class XCBuildConfiguration(XCObject):
- _schema = XCObject._schema.copy()
- _schema.update(
- {
- "baseConfigurationReference": [0, PBXFileReference, 0, 0],
- "buildSettings": [0, dict, 0, 1, {}],
- "name": [0, str, 0, 1],
- }
- )
- def HasBuildSetting(self, key):
- return key in self._properties["buildSettings"]
- def GetBuildSetting(self, key):
- return self._properties["buildSettings"][key]
- def SetBuildSetting(self, key, value):
- # TODO(mark): If a list, copy?
- self._properties["buildSettings"][key] = value
- def AppendBuildSetting(self, key, value):
- if key not in self._properties["buildSettings"]:
- self._properties["buildSettings"][key] = []
- self._properties["buildSettings"][key].append(value)
- def DelBuildSetting(self, key):
- if key in self._properties["buildSettings"]:
- del self._properties["buildSettings"][key]
- def SetBaseConfiguration(self, value):
- self._properties["baseConfigurationReference"] = value
- class XCConfigurationList(XCObject):
- # _configs is the default list of configurations.
- _configs = [
- XCBuildConfiguration({"name": "Debug"}),
- XCBuildConfiguration({"name": "Release"}),
- ]
- _schema = XCObject._schema.copy()
- _schema.update(
- {
- "buildConfigurations": [1, XCBuildConfiguration, 1, 1, _configs],
- "defaultConfigurationIsVisible": [0, int, 0, 1, 1],
- "defaultConfigurationName": [0, str, 0, 1, "Release"],
- }
- )
- def Name(self):
- return (
- "Build configuration list for "
- + self.parent.__class__.__name__
- + ' "'
- + self.parent.Name()
- + '"'
- )
- def ConfigurationNamed(self, name):
- """Convenience accessor to obtain an XCBuildConfiguration by name."""
- for configuration in self._properties["buildConfigurations"]:
- if configuration._properties["name"] == name:
- return configuration
- raise KeyError(name)
- def DefaultConfiguration(self):
- """Convenience accessor to obtain the default XCBuildConfiguration."""
- return self.ConfigurationNamed(self._properties["defaultConfigurationName"])
- def HasBuildSetting(self, key):
- """Determines the state of a build setting in all XCBuildConfiguration
- child objects.
- If all child objects have key in their build settings, and the value is the
- same in all child objects, returns 1.
- If no child objects have the key in their build settings, returns 0.
- If some, but not all, child objects have the key in their build settings,
- or if any children have different values for the key, returns -1.
- """
- has = None
- value = None
- for configuration in self._properties["buildConfigurations"]:
- configuration_has = configuration.HasBuildSetting(key)
- if has is None:
- has = configuration_has
- elif has != configuration_has:
- return -1
- if configuration_has:
- configuration_value = configuration.GetBuildSetting(key)
- if value is None:
- value = configuration_value
- elif value != configuration_value:
- return -1
- if not has:
- return 0
- return 1
- def GetBuildSetting(self, key):
- """Gets the build setting for key.
- All child XCConfiguration objects must have the same value set for the
- setting, or a ValueError will be raised.
- """
- # TODO(mark): This is wrong for build settings that are lists. The list
- # contents should be compared (and a list copy returned?)
- value = None
- for configuration in self._properties["buildConfigurations"]:
- configuration_value = configuration.GetBuildSetting(key)
- if value is None:
- value = configuration_value
- else:
- if value != configuration_value:
- raise ValueError("Variant values for " + key)
- return value
- def SetBuildSetting(self, key, value):
- """Sets the build setting for key to value in all child
- XCBuildConfiguration objects.
- """
- for configuration in self._properties["buildConfigurations"]:
- configuration.SetBuildSetting(key, value)
- def AppendBuildSetting(self, key, value):
- """Appends value to the build setting for key, which is treated as a list,
- in all child XCBuildConfiguration objects.
- """
- for configuration in self._properties["buildConfigurations"]:
- configuration.AppendBuildSetting(key, value)
- def DelBuildSetting(self, key):
- """Deletes the build setting key from all child XCBuildConfiguration
- objects.
- """
- for configuration in self._properties["buildConfigurations"]:
- configuration.DelBuildSetting(key)
- def SetBaseConfiguration(self, value):
- """Sets the build configuration in all child XCBuildConfiguration objects.
- """
- for configuration in self._properties["buildConfigurations"]:
- configuration.SetBaseConfiguration(value)
- class PBXBuildFile(XCObject):
- _schema = XCObject._schema.copy()
- _schema.update(
- {
- "fileRef": [0, XCFileLikeElement, 0, 1],
- "settings": [0, str, 0, 0], # hack, it's a dict
- }
- )
- # Weird output rules for PBXBuildFile.
- _should_print_single_line = True
- _encode_transforms = XCObject._alternate_encode_transforms
- def Name(self):
- # Example: "main.cc in Sources"
- return self._properties["fileRef"].Name() + " in " + self.parent.Name()
- def Hashables(self):
- # super
- hashables = XCObject.Hashables(self)
- # It is not sufficient to just rely on Name() to get the
- # XCFileLikeElement's name, because that is not a complete pathname.
- # PathHashables returns hashables unique enough that no two
- # PBXBuildFiles should wind up with the same set of hashables, unless
- # someone adds the same file multiple times to the same target. That
- # would be considered invalid anyway.
- hashables.extend(self._properties["fileRef"].PathHashables())
- return hashables
- class XCBuildPhase(XCObject):
- """Abstract base for build phase classes. Not represented in a project
- file.
- Attributes:
- _files_by_path: A dict mapping each path of a child in the files list by
- path (keys) to the corresponding PBXBuildFile children (values).
- _files_by_xcfilelikeelement: A dict mapping each XCFileLikeElement (keys)
- to the corresponding PBXBuildFile children (values).
- """
- # TODO(mark): Some build phase types, like PBXShellScriptBuildPhase, don't
- # actually have a "files" list. XCBuildPhase should not have "files" but
- # another abstract subclass of it should provide this, and concrete build
- # phase types that do have "files" lists should be derived from that new
- # abstract subclass. XCBuildPhase should only provide buildActionMask and
- # runOnlyForDeploymentPostprocessing, and not files or the various
- # file-related methods and attributes.
- _schema = XCObject._schema.copy()
- _schema.update(
- {
- "buildActionMask": [0, int, 0, 1, 0x7FFFFFFF],
- "files": [1, PBXBuildFile, 1, 1, []],
- "runOnlyForDeploymentPostprocessing": [0, int, 0, 1, 0],
- }
- )
- def __init__(self, properties=None, id=None, parent=None):
- # super
- XCObject.__init__(self, properties, id, parent)
- self._files_by_path = {}
- self._files_by_xcfilelikeelement = {}
- for pbxbuildfile in self._properties.get("files", []):
- self._AddBuildFileToDicts(pbxbuildfile)
- def FileGroup(self, path):
- # Subclasses must override this by returning a two-element tuple. The
- # first item in the tuple should be the PBXGroup to which "path" should be
- # added, either as a child or deeper descendant. The second item should
- # be a boolean indicating whether files should be added into hierarchical
- # groups or one single flat group.
- raise NotImplementedError(self.__class__.__name__ + " must implement FileGroup")
- def _AddPathToDict(self, pbxbuildfile, path):
- """Adds path to the dict tracking paths belonging to this build phase.
- If the path is already a member of this build phase, raises an exception.
- """
- if path in self._files_by_path:
- raise ValueError("Found multiple build files with path " + path)
- self._files_by_path[path] = pbxbuildfile
- def _AddBuildFileToDicts(self, pbxbuildfile, path=None):
- """Maintains the _files_by_path and _files_by_xcfilelikeelement dicts.
- If path is specified, then it is the path that is being added to the
- phase, and pbxbuildfile must contain either a PBXFileReference directly
- referencing that path, or it must contain a PBXVariantGroup that itself
- contains a PBXFileReference referencing the path.
- If path is not specified, either the PBXFileReference's path or the paths
- of all children of the PBXVariantGroup are taken as being added to the
- phase.
- If the path is already present in the phase, raises an exception.
- If the PBXFileReference or PBXVariantGroup referenced by pbxbuildfile
- are already present in the phase, referenced by a different PBXBuildFile
- object, raises an exception. This does not raise an exception when
- a PBXFileReference or PBXVariantGroup reappear and are referenced by the
- same PBXBuildFile that has already introduced them, because in the case
- of PBXVariantGroup objects, they may correspond to multiple paths that are
- not all added simultaneously. When this situation occurs, the path needs
- to be added to _files_by_path, but nothing needs to change in
- _files_by_xcfilelikeelement, and the caller should have avoided adding
- the PBXBuildFile if it is already present in the list of children.
- """
- xcfilelikeelement = pbxbuildfile._properties["fileRef"]
- paths = []
- if path is not None:
- # It's best when the caller provides the path.
- if isinstance(xcfilelikeelement, PBXVariantGroup):
- paths.append(path)
- else:
- # If the caller didn't provide a path, there can be either multiple
- # paths (PBXVariantGroup) or one.
- if isinstance(xcfilelikeelement, PBXVariantGroup):
- for variant in xcfilelikeelement._properties["children"]:
- paths.append(variant.FullPath())
- else:
- paths.append(xcfilelikeelement.FullPath())
- # Add the paths first, because if something's going to raise, the
- # messages provided by _AddPathToDict are more useful owing to its
- # having access to a real pathname and not just an object's Name().
- for a_path in paths:
- self._AddPathToDict(pbxbuildfile, a_path)
- # If another PBXBuildFile references this XCFileLikeElement, there's a
- # problem.
- if (
- xcfilelikeelement in self._files_by_xcfilelikeelement
- and self._files_by_xcfilelikeelement[xcfilelikeelement] != pbxbuildfile
- ):
- raise ValueError(
- "Found multiple build files for " + xcfilelikeelement.Name()
- )
- self._files_by_xcfilelikeelement[xcfilelikeelement] = pbxbuildfile
- def AppendBuildFile(self, pbxbuildfile, path=None):
- # Callers should use this instead of calling
- # AppendProperty('files', pbxbuildfile) directly because this function
- # maintains the object's dicts. Better yet, callers can just call AddFile
- # with a pathname and not worry about building their own PBXBuildFile
- # objects.
- self.AppendProperty("files", pbxbuildfile)
- self._AddBuildFileToDicts(pbxbuildfile, path)
- def AddFile(self, path, settings=None):
- (file_group, hierarchical) = self.FileGroup(path)
- file_ref = file_group.AddOrGetFileByPath(path, hierarchical)
- if file_ref in self._files_by_xcfilelikeelement and isinstance(
- file_ref, PBXVariantGroup
- ):
- # There's already a PBXBuildFile in this phase corresponding to the
- # PBXVariantGroup. path just provides a new variant that belongs to
- # the group. Add the path to the dict.
- pbxbuildfile = self._files_by_xcfilelikeelement[file_ref]
- self._AddBuildFileToDicts(pbxbuildfile, path)
- else:
- # Add a new PBXBuildFile to get file_ref into the phase.
- if settings is None:
- pbxbuildfile = PBXBuildFile({"fileRef": file_ref})
- else:
- pbxbuildfile = PBXBuildFile({"fileRef": file_ref, "settings": settings})
- self.AppendBuildFile(pbxbuildfile, path)
- class PBXHeadersBuildPhase(XCBuildPhase):
- # No additions to the schema relative to XCBuildPhase.
- def Name(self):
- return "Headers"
- def FileGroup(self, path):
- return self.PBXProjectAncestor().RootGroupForPath(path)
- class PBXResourcesBuildPhase(XCBuildPhase):
- # No additions to the schema relative to XCBuildPhase.
- def Name(self):
- return "Resources"
- def FileGroup(self, path):
- return self.PBXProjectAncestor().RootGroupForPath(path)
- class PBXSourcesBuildPhase(XCBuildPhase):
- # No additions to the schema relative to XCBuildPhase.
- def Name(self):
- return "Sources"
- def FileGroup(self, path):
- return self.PBXProjectAncestor().RootGroupForPath(path)
- class PBXFrameworksBuildPhase(XCBuildPhase):
- # No additions to the schema relative to XCBuildPhase.
- def Name(self):
- return "Frameworks"
- def FileGroup(self, path):
- (root, ext) = posixpath.splitext(path)
- if ext != "":
- ext = ext[1:].lower()
- if ext == "o":
- # .o files are added to Xcode Frameworks phases, but conceptually aren't
- # frameworks, they're more like sources or intermediates. Redirect them
- # to show up in one of those other groups.
- return self.PBXProjectAncestor().RootGroupForPath(path)
- else:
- return (self.PBXProjectAncestor().FrameworksGroup(), False)
- class PBXShellScriptBuildPhase(XCBuildPhase):
- _schema = XCBuildPhase._schema.copy()
- _schema.update(
- {
- "inputPaths": [1, str, 0, 1, []],
- "name": [0, str, 0, 0],
- "outputPaths": [1, str, 0, 1, []],
- "shellPath": [0, str, 0, 1, "/bin/sh"],
- "shellScript": [0, str, 0, 1],
- "showEnvVarsInLog": [0, int, 0, 0],
- }
- )
- def Name(self):
- if "name" in self._properties:
- return self._properties["name"]
- return "ShellScript"
- class PBXCopyFilesBuildPhase(XCBuildPhase):
- _schema = XCBuildPhase._schema.copy()
- _schema.update(
- {
- "dstPath": [0, str, 0, 1],
- "dstSubfolderSpec": [0, int, 0, 1],
- "name": [0, str, 0, 0],
- }
- )
- # path_tree_re matches "$(DIR)/path", "$(DIR)/$(DIR2)/path" or just "$(DIR)".
- # Match group 1 is "DIR", group 3 is "path" or "$(DIR2") or "$(DIR2)/path"
- # or None. If group 3 is "path", group 4 will be None otherwise group 4 is
- # "DIR2" and group 6 is "path".
- path_tree_re = re.compile(r"^\$\((.*?)\)(/(\$\((.*?)\)(/(.*)|)|(.*)|)|)$")
- # path_tree_{first,second}_to_subfolder map names of Xcode variables to the
- # associated dstSubfolderSpec property value used in a PBXCopyFilesBuildPhase
- # object.
- path_tree_first_to_subfolder = {
- # Types that can be chosen via the Xcode UI.
- "BUILT_PRODUCTS_DIR": 16, # Products Directory
- "BUILT_FRAMEWORKS_DIR": 10, # Not an official Xcode macro.
- # Existed before support for the
- # names below was added. Maps to
- # "Frameworks".
- }
- path_tree_second_to_subfolder = {
- "WRAPPER_NAME": 1, # Wrapper
- # Although Xcode's friendly name is "Executables", the destination
- # is demonstrably the value of the build setting
- # EXECUTABLE_FOLDER_PATH not EXECUTABLES_FOLDER_PATH.
- "EXECUTABLE_FOLDER_PATH": 6, # Executables.
- "UNLOCALIZED_RESOURCES_FOLDER_PATH": 7, # Resources
- "JAVA_FOLDER_PATH": 15, # Java Resources
- "FRAMEWORKS_FOLDER_PATH": 10, # Frameworks
- "SHARED_FRAMEWORKS_FOLDER_PATH": 11, # Shared Frameworks
- "SHARED_SUPPORT_FOLDER_PATH": 12, # Shared Support
- "PLUGINS_FOLDER_PATH": 13, # PlugIns
- # For XPC Services, Xcode sets both dstPath and dstSubfolderSpec.
- # Note that it re-uses the BUILT_PRODUCTS_DIR value for
- # dstSubfolderSpec. dstPath is set below.
- "XPCSERVICES_FOLDER_PATH": 16, # XPC Services.
- }
- def Name(self):
- if "name" in self._properties:
- return self._properties["name"]
- return "CopyFiles"
- def FileGroup(self, path):
- return self.PBXProjectAncestor().RootGroupForPath(path)
- def SetDestination(self, path):
- """Set the dstSubfolderSpec and dstPath properties from path.
- path may be specified in the same notation used for XCHierarchicalElements,
- specifically, "$(DIR)/path".
- """
- path_tree_match = self.path_tree_re.search(path)
- if path_tree_match:
- path_tree = path_tree_match.group(1)
- if path_tree in self.path_tree_first_to_subfolder:
- subfolder = self.path_tree_first_to_subfolder[path_tree]
- relative_path = path_tree_match.group(3)
- if relative_path is None:
- relative_path = ""
- if subfolder == 16 and path_tree_match.group(4) is not None:
- # BUILT_PRODUCTS_DIR (16) is the first element in a path whose
- # second element is possibly one of the variable names in
- # path_tree_second_to_subfolder. Xcode sets the values of all these
- # variables to relative paths so .gyp files must prefix them with
- # BUILT_PRODUCTS_DIR, e.g.
- # $(BUILT_PRODUCTS_DIR)/$(PLUGINS_FOLDER_PATH). Then
- # xcode_emulation.py can export these variables with the same values
- # as Xcode yet make & ninja files can determine the absolute path
- # to the target. Xcode uses the dstSubfolderSpec value set here
- # to determine the full path.
- #
- # An alternative of xcode_emulation.py setting the values to
- # absolute paths when exporting these variables has been
- # ruled out because then the values would be different
- # depending on the build tool.
- #
- # Another alternative is to invent new names for the variables used
- # to match to the subfolder indices in the second table. .gyp files
- # then will not need to prepend $(BUILT_PRODUCTS_DIR) because
- # xcode_emulation.py can set the values of those variables to
- # the absolute paths when exporting. This is possibly the thinking
- # behind BUILT_FRAMEWORKS_DIR which is used in exactly this manner.
- #
- # Requiring prepending BUILT_PRODUCTS_DIR has been chosen because
- # this same way could be used to specify destinations in .gyp files
- # that pre-date this addition to GYP. However they would only work
- # with the Xcode generator.
- # The previous version of xcode_emulation.py
- # does not export these variables. Such files will get the benefit
- # of the Xcode UI showing the proper destination name simply by
- # regenerating the projects with this version of GYP.
- path_tree = path_tree_match.group(4)
- relative_path = path_tree_match.group(6)
- separator = "/"
- if path_tree in self.path_tree_second_to_subfolder:
- subfolder = self.path_tree_second_to_subfolder[path_tree]
- if relative_path is None:
- relative_path = ""
- separator = ""
- if path_tree == "XPCSERVICES_FOLDER_PATH":
- relative_path = (
- "$(CONTENTS_FOLDER_PATH)/XPCServices"
- + separator
- + relative_path
- )
- else:
- # subfolder = 16 from above
- # The second element of the path is an unrecognized variable.
- # Include it and any remaining elements in relative_path.
- relative_path = path_tree_match.group(3)
- else:
- # The path starts with an unrecognized Xcode variable
- # name like $(SRCROOT). Xcode will still handle this
- # as an "absolute path" that starts with the variable.
- subfolder = 0
- relative_path = path
- elif path.startswith("/"):
- # Special case. Absolute paths are in dstSubfolderSpec 0.
- subfolder = 0
- relative_path = path[1:]
- else:
- raise ValueError(
- f"Can't use path {path} in a {self.__class__.__name__}"
- )
- self._properties["dstPath"] = relative_path
- self._properties["dstSubfolderSpec"] = subfolder
- class PBXBuildRule(XCObject):
- _schema = XCObject._schema.copy()
- _schema.update(
- {
- "compilerSpec": [0, str, 0, 1],
- "filePatterns": [0, str, 0, 0],
- "fileType": [0, str, 0, 1],
- "isEditable": [0, int, 0, 1, 1],
- "outputFiles": [1, str, 0, 1, []],
- "script": [0, str, 0, 0],
- }
- )
- def Name(self):
- # Not very inspired, but it's what Xcode uses.
- return self.__class__.__name__
- def Hashables(self):
- # super
- hashables = XCObject.Hashables(self)
- # Use the hashables of the weak objects that this object refers to.
- hashables.append(self._properties["fileType"])
- if "filePatterns" in self._properties:
- hashables.append(self._properties["filePatterns"])
- return hashables
- class PBXContainerItemProxy(XCObject):
- # When referencing an item in this project file, containerPortal is the
- # PBXProject root object of this project file. When referencing an item in
- # another project file, containerPortal is a PBXFileReference identifying
- # the other project file.
- #
- # When serving as a proxy to an XCTarget (in this project file or another),
- # proxyType is 1. When serving as a proxy to a PBXFileReference (in another
- # project file), proxyType is 2. Type 2 is used for references to the
- # producs of the other project file's targets.
- #
- # Xcode is weird about remoteGlobalIDString. Usually, it's printed without
- # a comment, indicating that it's tracked internally simply as a string, but
- # sometimes it's printed with a comment (usually when the object is initially
- # created), indicating that it's tracked as a project file object at least
- # sometimes. This module always tracks it as an object, but contains a hack
- # to prevent it from printing the comment in the project file output. See
- # _XCKVPrint.
- _schema = XCObject._schema.copy()
- _schema.update(
- {
- "containerPortal": [0, XCContainerPortal, 0, 1],
- "proxyType": [0, int, 0, 1],
- "remoteGlobalIDString": [0, XCRemoteObject, 0, 1],
- "remoteInfo": [0, str, 0, 1],
- }
- )
- def __repr__(self):
- props = self._properties
- name = "{}.gyp:{}".format(props["containerPortal"].Name(), props["remoteInfo"])
- return f"<{self.__class__.__name__} {name!r} at 0x{id(self):x}>"
- def Name(self):
- # Admittedly not the best name, but it's what Xcode uses.
- return self.__class__.__name__
- def Hashables(self):
- # super
- hashables = XCObject.Hashables(self)
- # Use the hashables of the weak objects that this object refers to.
- hashables.extend(self._properties["containerPortal"].Hashables())
- hashables.extend(self._properties["remoteGlobalIDString"].Hashables())
- return hashables
- class PBXTargetDependency(XCObject):
- # The "target" property accepts an XCTarget object, and obviously not
- # NoneType. But XCTarget is defined below, so it can't be put into the
- # schema yet. The definition of PBXTargetDependency can't be moved below
- # XCTarget because XCTarget's own schema references PBXTargetDependency.
- # Python doesn't deal well with this circular relationship, and doesn't have
- # a real way to do forward declarations. To work around, the type of
- # the "target" property is reset below, after XCTarget is defined.
- #
- # At least one of "name" and "target" is required.
- _schema = XCObject._schema.copy()
- _schema.update(
- {
- "name": [0, str, 0, 0],
- "target": [0, None.__class__, 0, 0],
- "targetProxy": [0, PBXContainerItemProxy, 1, 1],
- }
- )
- def __repr__(self):
- name = self._properties.get("name") or self._properties["target"].Name()
- return f"<{self.__class__.__name__} {name!r} at 0x{id(self):x}>"
- def Name(self):
- # Admittedly not the best name, but it's what Xcode uses.
- return self.__class__.__name__
- def Hashables(self):
- # super
- hashables = XCObject.Hashables(self)
- # Use the hashables of the weak objects that this object refers to.
- hashables.extend(self._properties["targetProxy"].Hashables())
- return hashables
- class PBXReferenceProxy(XCFileLikeElement):
- _schema = XCFileLikeElement._schema.copy()
- _schema.update(
- {
- "fileType": [0, str, 0, 1],
- "path": [0, str, 0, 1],
- "remoteRef": [0, PBXContainerItemProxy, 1, 1],
- }
- )
- class XCTarget(XCRemoteObject):
- # An XCTarget is really just an XCObject, the XCRemoteObject thing is just
- # to allow PBXProject to be used in the remoteGlobalIDString property of
- # PBXContainerItemProxy.
- #
- # Setting a "name" property at instantiation may also affect "productName",
- # which may in turn affect the "PRODUCT_NAME" build setting in children of
- # "buildConfigurationList". See __init__ below.
- _schema = XCRemoteObject._schema.copy()
- _schema.update(
- {
- "buildConfigurationList": [
- 0,
- XCConfigurationList,
- 1,
- 1,
- XCConfigurationList(),
- ],
- "buildPhases": [1, XCBuildPhase, 1, 1, []],
- "dependencies": [1, PBXTargetDependency, 1, 1, []],
- "name": [0, str, 0, 1],
- "productName": [0, str, 0, 1],
- }
- )
- def __init__(
- self,
- properties=None,
- id=None,
- parent=None,
- force_outdir=None,
- force_prefix=None,
- force_extension=None,
- ):
- # super
- XCRemoteObject.__init__(self, properties, id, parent)
- # Set up additional defaults not expressed in the schema. If a "name"
- # property was supplied, set "productName" if it is not present. Also set
- # the "PRODUCT_NAME" build setting in each configuration, but only if
- # the setting is not present in any build configuration.
- if "name" in self._properties:
- if "productName" not in self._properties:
- self.SetProperty("productName", self._properties["name"])
- if "productName" in self._properties:
- if "buildConfigurationList" in self._properties:
- configs = self._properties["buildConfigurationList"]
- if configs.HasBuildSetting("PRODUCT_NAME") == 0:
- configs.SetBuildSetting(
- "PRODUCT_NAME", self._properties["productName"]
- )
- def AddDependency(self, other):
- pbxproject = self.PBXProjectAncestor()
- other_pbxproject = other.PBXProjectAncestor()
- if pbxproject == other_pbxproject:
- # Add a dependency to another target in the same project file.
- container = PBXContainerItemProxy(
- {
- "containerPortal": pbxproject,
- "proxyType": 1,
- "remoteGlobalIDString": other,
- "remoteInfo": other.Name(),
- }
- )
- dependency = PBXTargetDependency(
- {"target": other, "targetProxy": container}
- )
- self.AppendProperty("dependencies", dependency)
- else:
- # Add a dependency to a target in a different project file.
- other_project_ref = pbxproject.AddOrGetProjectReference(other_pbxproject)[1]
- container = PBXContainerItemProxy(
- {
- "containerPortal": other_project_ref,
- "proxyType": 1,
- "remoteGlobalIDString": other,
- "remoteInfo": other.Name(),
- }
- )
- dependency = PBXTargetDependency(
- {"name": other.Name(), "targetProxy": container}
- )
- self.AppendProperty("dependencies", dependency)
- # Proxy all of these through to the build configuration list.
- def ConfigurationNamed(self, name):
- return self._properties["buildConfigurationList"].ConfigurationNamed(name)
- def DefaultConfiguration(self):
- return self._properties["buildConfigurationList"].DefaultConfiguration()
- def HasBuildSetting(self, key):
- return self._properties["buildConfigurationList"].HasBuildSetting(key)
- def GetBuildSetting(self, key):
- return self._properties["buildConfigurationList"].GetBuildSetting(key)
- def SetBuildSetting(self, key, value):
- return self._properties["buildConfigurationList"].SetBuildSetting(key, value)
- def AppendBuildSetting(self, key, value):
- return self._properties["buildConfigurationList"].AppendBuildSetting(key, value)
- def DelBuildSetting(self, key):
- return self._properties["buildConfigurationList"].DelBuildSetting(key)
- # Redefine the type of the "target" property. See PBXTargetDependency._schema
- # above.
- PBXTargetDependency._schema["target"][1] = XCTarget
- class PBXNativeTarget(XCTarget):
- # buildPhases is overridden in the schema to be able to set defaults.
- #
- # NOTE: Contrary to most objects, it is advisable to set parent when
- # constructing PBXNativeTarget. A parent of an XCTarget must be a PBXProject
- # object. A parent reference is required for a PBXNativeTarget during
- # construction to be able to set up the target defaults for productReference,
- # because a PBXBuildFile object must be created for the target and it must
- # be added to the PBXProject's mainGroup hierarchy.
- _schema = XCTarget._schema.copy()
- _schema.update(
- {
- "buildPhases": [
- 1,
- XCBuildPhase,
- 1,
- 1,
- [PBXSourcesBuildPhase(), PBXFrameworksBuildPhase()],
- ],
- "buildRules": [1, PBXBuildRule, 1, 1, []],
- "productReference": [0, PBXFileReference, 0, 1],
- "productType": [0, str, 0, 1],
- }
- )
- # Mapping from Xcode product-types to settings. The settings are:
- # filetype : used for explicitFileType in the project file
- # prefix : the prefix for the file name
- # suffix : the suffix for the file name
- _product_filetypes = {
- "com.apple.product-type.application": ["wrapper.application", "", ".app"],
- "com.apple.product-type.application.watchapp": [
- "wrapper.application",
- "",
- ".app",
- ],
- "com.apple.product-type.watchkit-extension": [
- "wrapper.app-extension",
- "",
- ".appex",
- ],
- "com.apple.product-type.app-extension": ["wrapper.app-extension", "", ".appex"],
- "com.apple.product-type.bundle": ["wrapper.cfbundle", "", ".bundle"],
- "com.apple.product-type.framework": ["wrapper.framework", "", ".framework"],
- "com.apple.product-type.library.dynamic": [
- "compiled.mach-o.dylib",
- "lib",
- ".dylib",
- ],
- "com.apple.product-type.library.static": ["archive.ar", "lib", ".a"],
- "com.apple.product-type.tool": ["compiled.mach-o.executable", "", ""],
- "com.apple.product-type.bundle.unit-test": ["wrapper.cfbundle", "", ".xctest"],
- "com.apple.product-type.bundle.ui-testing": ["wrapper.cfbundle", "", ".xctest"],
- "com.googlecode.gyp.xcode.bundle": ["compiled.mach-o.dylib", "", ".so"],
- "com.apple.product-type.kernel-extension": ["wrapper.kext", "", ".kext"],
- }
- def __init__(
- self,
- properties=None,
- id=None,
- parent=None,
- force_outdir=None,
- force_prefix=None,
- force_extension=None,
- ):
- # super
- XCTarget.__init__(self, properties, id, parent)
- if (
- "productName" in self._properties
- and "productType" in self._properties
- and "productReference" not in self._properties
- and self._properties["productType"] in self._product_filetypes
- ):
- products_group = None
- pbxproject = self.PBXProjectAncestor()
- if pbxproject is not None:
- products_group = pbxproject.ProductsGroup()
- if products_group is not None:
- (filetype, prefix, suffix) = self._product_filetypes[
- self._properties["productType"]
- ]
- # Xcode does not have a distinct type for loadable modules that are
- # pure BSD targets (not in a bundle wrapper). GYP allows such modules
- # to be specified by setting a target type to loadable_module without
- # having mac_bundle set. These are mapped to the pseudo-product type
- # com.googlecode.gyp.xcode.bundle.
- #
- # By picking up this special type and converting it to a dynamic
- # library (com.apple.product-type.library.dynamic) with fix-ups,
- # single-file loadable modules can be produced.
- #
- # MACH_O_TYPE is changed to mh_bundle to produce the proper file type
- # (as opposed to mh_dylib). In order for linking to succeed,
- # DYLIB_CURRENT_VERSION and DYLIB_COMPATIBILITY_VERSION must be
- # cleared. They are meaningless for type mh_bundle.
- #
- # Finally, the .so extension is forcibly applied over the default
- # (.dylib), unless another forced extension is already selected.
- # .dylib is plainly wrong, and .bundle is used by loadable_modules in
- # bundle wrappers (com.apple.product-type.bundle). .so seems an odd
- # choice because it's used as the extension on many other systems that
- # don't distinguish between linkable shared libraries and non-linkable
- # loadable modules, but there's precedent: Python loadable modules on
- # Mac OS X use an .so extension.
- if self._properties["productType"] == "com.googlecode.gyp.xcode.bundle":
- self._properties[
- "productType"
- ] = "com.apple.product-type.library.dynamic"
- self.SetBuildSetting("MACH_O_TYPE", "mh_bundle")
- self.SetBuildSetting("DYLIB_CURRENT_VERSION", "")
- self.SetBuildSetting("DYLIB_COMPATIBILITY_VERSION", "")
- if force_extension is None:
- force_extension = suffix[1:]
- if (
- self._properties["productType"]
- == "com.apple.product-type-bundle.unit.test"
- or self._properties["productType"]
- == "com.apple.product-type-bundle.ui-testing"
- ):
- if force_extension is None:
- force_extension = suffix[1:]
- if force_extension is not None:
- # If it's a wrapper (bundle), set WRAPPER_EXTENSION.
- # Extension override.
- suffix = "." + force_extension
- if filetype.startswith("wrapper."):
- self.SetBuildSetting("WRAPPER_EXTENSION", force_extension)
- else:
- self.SetBuildSetting("EXECUTABLE_EXTENSION", force_extension)
- if filetype.startswith("compiled.mach-o.executable"):
- product_name = self._properties["productName"]
- product_name += suffix
- suffix = ""
- self.SetProperty("productName", product_name)
- self.SetBuildSetting("PRODUCT_NAME", product_name)
- # Xcode handles most prefixes based on the target type, however there
- # are exceptions. If a "BSD Dynamic Library" target is added in the
- # Xcode UI, Xcode sets EXECUTABLE_PREFIX. This check duplicates that
- # behavior.
- if force_prefix is not None:
- prefix = force_prefix
- if filetype.startswith("wrapper."):
- self.SetBuildSetting("WRAPPER_PREFIX", prefix)
- else:
- self.SetBuildSetting("EXECUTABLE_PREFIX", prefix)
- if force_outdir is not None:
- self.SetBuildSetting("TARGET_BUILD_DIR", force_outdir)
- # TODO(tvl): Remove the below hack.
- # http://code.google.com/p/gyp/issues/detail?id=122
- # Some targets include the prefix in the target_name. These targets
- # really should just add a product_name setting that doesn't include
- # the prefix. For example:
- # target_name = 'libevent', product_name = 'event'
- # This check cleans up for them.
- product_name = self._properties["productName"]
- prefix_len = len(prefix)
- if prefix_len and (product_name[:prefix_len] == prefix):
- product_name = product_name[prefix_len:]
- self.SetProperty("productName", product_name)
- self.SetBuildSetting("PRODUCT_NAME", product_name)
- ref_props = {
- "explicitFileType": filetype,
- "includeInIndex": 0,
- "path": prefix + product_name + suffix,
- "sourceTree": "BUILT_PRODUCTS_DIR",
- }
- file_ref = PBXFileReference(ref_props)
- products_group.AppendChild(file_ref)
- self.SetProperty("productReference", file_ref)
- def GetBuildPhaseByType(self, type):
- if "buildPhases" not in self._properties:
- return None
- the_phase = None
- for phase in self._properties["buildPhases"]:
- if isinstance(phase, type):
- # Some phases may be present in multiples in a well-formed project file,
- # but phases like PBXSourcesBuildPhase may only be present singly, and
- # this function is intended as an aid to GetBuildPhaseByType. Loop
- # over the entire list of phases and assert if more than one of the
- # desired type is found.
- assert the_phase is None
- the_phase = phase
- return the_phase
- def HeadersPhase(self):
- headers_phase = self.GetBuildPhaseByType(PBXHeadersBuildPhase)
- if headers_phase is None:
- headers_phase = PBXHeadersBuildPhase()
- # The headers phase should come before the resources, sources, and
- # frameworks phases, if any.
- insert_at = len(self._properties["buildPhases"])
- for index, phase in enumerate(self._properties["buildPhases"]):
- if (
- isinstance(phase, PBXResourcesBuildPhase)
- or isinstance(phase, PBXSourcesBuildPhase)
- or isinstance(phase, PBXFrameworksBuildPhase)
- ):
- insert_at = index
- break
- self._properties["buildPhases"].insert(insert_at, headers_phase)
- headers_phase.parent = self
- return headers_phase
- def ResourcesPhase(self):
- resources_phase = self.GetBuildPhaseByType(PBXResourcesBuildPhase)
- if resources_phase is None:
- resources_phase = PBXResourcesBuildPhase()
- # The resources phase should come before the sources and frameworks
- # phases, if any.
- insert_at = len(self._properties["buildPhases"])
- for index, phase in enumerate(self._properties["buildPhases"]):
- if isinstance(phase, PBXSourcesBuildPhase) or isinstance(
- phase, PBXFrameworksBuildPhase
- ):
- insert_at = index
- break
- self._properties["buildPhases"].insert(insert_at, resources_phase)
- resources_phase.parent = self
- return resources_phase
- def SourcesPhase(self):
- sources_phase = self.GetBuildPhaseByType(PBXSourcesBuildPhase)
- if sources_phase is None:
- sources_phase = PBXSourcesBuildPhase()
- self.AppendProperty("buildPhases", sources_phase)
- return sources_phase
- def FrameworksPhase(self):
- frameworks_phase = self.GetBuildPhaseByType(PBXFrameworksBuildPhase)
- if frameworks_phase is None:
- frameworks_phase = PBXFrameworksBuildPhase()
- self.AppendProperty("buildPhases", frameworks_phase)
- return frameworks_phase
- def AddDependency(self, other):
- # super
- XCTarget.AddDependency(self, other)
- static_library_type = "com.apple.product-type.library.static"
- shared_library_type = "com.apple.product-type.library.dynamic"
- framework_type = "com.apple.product-type.framework"
- if (
- isinstance(other, PBXNativeTarget)
- and "productType" in self._properties
- and self._properties["productType"] != static_library_type
- and "productType" in other._properties
- and (
- other._properties["productType"] == static_library_type
- or (
- (
- other._properties["productType"] == shared_library_type
- or other._properties["productType"] == framework_type
- )
- and (
- (not other.HasBuildSetting("MACH_O_TYPE"))
- or other.GetBuildSetting("MACH_O_TYPE") != "mh_bundle"
- )
- )
- )
- ):
- file_ref = other.GetProperty("productReference")
- pbxproject = self.PBXProjectAncestor()
- other_pbxproject = other.PBXProjectAncestor()
- if pbxproject != other_pbxproject:
- other_project_product_group = pbxproject.AddOrGetProjectReference(
- other_pbxproject
- )[0]
- file_ref = other_project_product_group.GetChildByRemoteObject(file_ref)
- self.FrameworksPhase().AppendProperty(
- "files", PBXBuildFile({"fileRef": file_ref})
- )
- class PBXAggregateTarget(XCTarget):
- pass
- class PBXProject(XCContainerPortal):
- # A PBXProject is really just an XCObject, the XCContainerPortal thing is
- # just to allow PBXProject to be used in the containerPortal property of
- # PBXContainerItemProxy.
- """
- Attributes:
- path: "sample.xcodeproj". TODO(mark) Document me!
- _other_pbxprojects: A dictionary, keyed by other PBXProject objects. Each
- value is a reference to the dict in the
- projectReferences list associated with the keyed
- PBXProject.
- """
- _schema = XCContainerPortal._schema.copy()
- _schema.update(
- {
- "attributes": [0, dict, 0, 0],
- "buildConfigurationList": [
- 0,
- XCConfigurationList,
- 1,
- 1,
- XCConfigurationList(),
- ],
- "compatibilityVersion": [0, str, 0, 1, "Xcode 3.2"],
- "hasScannedForEncodings": [0, int, 0, 1, 1],
- "mainGroup": [0, PBXGroup, 1, 1, PBXGroup()],
- "projectDirPath": [0, str, 0, 1, ""],
- "projectReferences": [1, dict, 0, 0],
- "projectRoot": [0, str, 0, 1, ""],
- "targets": [1, XCTarget, 1, 1, []],
- }
- )
- def __init__(self, properties=None, id=None, parent=None, path=None):
- self.path = path
- self._other_pbxprojects = {}
- # super
- return XCContainerPortal.__init__(self, properties, id, parent)
- def Name(self):
- name = self.path
- if name[-10:] == ".xcodeproj":
- name = name[:-10]
- return posixpath.basename(name)
- def Path(self):
- return self.path
- def Comment(self):
- return "Project object"
- def Children(self):
- # super
- children = XCContainerPortal.Children(self)
- # Add children that the schema doesn't know about. Maybe there's a more
- # elegant way around this, but this is the only case where we need to own
- # objects in a dictionary (that is itself in a list), and three lines for
- # a one-off isn't that big a deal.
- if "projectReferences" in self._properties:
- for reference in self._properties["projectReferences"]:
- children.append(reference["ProductGroup"])
- return children
- def PBXProjectAncestor(self):
- return self
- def _GroupByName(self, name):
- if "mainGroup" not in self._properties:
- self.SetProperty("mainGroup", PBXGroup())
- main_group = self._properties["mainGroup"]
- group = main_group.GetChildByName(name)
- if group is None:
- group = PBXGroup({"name": name})
- main_group.AppendChild(group)
- return group
- # SourceGroup and ProductsGroup are created by default in Xcode's own
- # templates.
- def SourceGroup(self):
- return self._GroupByName("Source")
- def ProductsGroup(self):
- return self._GroupByName("Products")
- # IntermediatesGroup is used to collect source-like files that are generated
- # by rules or script phases and are placed in intermediate directories such
- # as DerivedSources.
- def IntermediatesGroup(self):
- return self._GroupByName("Intermediates")
- # FrameworksGroup and ProjectsGroup are top-level groups used to collect
- # frameworks and projects.
- def FrameworksGroup(self):
- return self._GroupByName("Frameworks")
- def ProjectsGroup(self):
- return self._GroupByName("Projects")
- def RootGroupForPath(self, path):
- """Returns a PBXGroup child of this object to which path should be added.
- This method is intended to choose between SourceGroup and
- IntermediatesGroup on the basis of whether path is present in a source
- directory or an intermediates directory. For the purposes of this
- determination, any path located within a derived file directory such as
- PROJECT_DERIVED_FILE_DIR is treated as being in an intermediates
- directory.
- The returned value is a two-element tuple. The first element is the
- PBXGroup, and the second element specifies whether that group should be
- organized hierarchically (True) or as a single flat list (False).
- """
- # TODO(mark): make this a class variable and bind to self on call?
- # Also, this list is nowhere near exhaustive.
- # INTERMEDIATE_DIR and SHARED_INTERMEDIATE_DIR are used by
- # gyp.generator.xcode. There should probably be some way for that module
- # to push the names in, rather than having to hard-code them here.
- source_tree_groups = {
- "DERIVED_FILE_DIR": (self.IntermediatesGroup, True),
- "INTERMEDIATE_DIR": (self.IntermediatesGroup, True),
- "PROJECT_DERIVED_FILE_DIR": (self.IntermediatesGroup, True),
- "SHARED_INTERMEDIATE_DIR": (self.IntermediatesGroup, True),
- }
- (source_tree, path) = SourceTreeAndPathFromPath(path)
- if source_tree is not None and source_tree in source_tree_groups:
- (group_func, hierarchical) = source_tree_groups[source_tree]
- group = group_func()
- return (group, hierarchical)
- # TODO(mark): make additional choices based on file extension.
- return (self.SourceGroup(), True)
- def AddOrGetFileInRootGroup(self, path):
- """Returns a PBXFileReference corresponding to path in the correct group
- according to RootGroupForPath's heuristics.
- If an existing PBXFileReference for path exists, it will be returned.
- Otherwise, one will be created and returned.
- """
- (group, hierarchical) = self.RootGroupForPath(path)
- return group.AddOrGetFileByPath(path, hierarchical)
- def RootGroupsTakeOverOnlyChildren(self, recurse=False):
- """Calls TakeOverOnlyChild for all groups in the main group."""
- for group in self._properties["mainGroup"]._properties["children"]:
- if isinstance(group, PBXGroup):
- group.TakeOverOnlyChild(recurse)
- def SortGroups(self):
- # Sort the children of the mainGroup (like "Source" and "Products")
- # according to their defined order.
- self._properties["mainGroup"]._properties["children"] = sorted(
- self._properties["mainGroup"]._properties["children"],
- key=cmp_to_key(lambda x, y: x.CompareRootGroup(y)),
- )
- # Sort everything else by putting group before files, and going
- # alphabetically by name within sections of groups and files. SortGroup
- # is recursive.
- for group in self._properties["mainGroup"]._properties["children"]:
- if not isinstance(group, PBXGroup):
- continue
- if group.Name() == "Products":
- # The Products group is a special case. Instead of sorting
- # alphabetically, sort things in the order of the targets that
- # produce the products. To do this, just build up a new list of
- # products based on the targets.
- products = []
- for target in self._properties["targets"]:
- if not isinstance(target, PBXNativeTarget):
- continue
- product = target._properties["productReference"]
- # Make sure that the product is already in the products group.
- assert product in group._properties["children"]
- products.append(product)
- # Make sure that this process doesn't miss anything that was already
- # in the products group.
- assert len(products) == len(group._properties["children"])
- group._properties["children"] = products
- else:
- group.SortGroup()
- def AddOrGetProjectReference(self, other_pbxproject):
- """Add a reference to another project file (via PBXProject object) to this
- one.
- Returns [ProductGroup, ProjectRef]. ProductGroup is a PBXGroup object in
- this project file that contains a PBXReferenceProxy object for each
- product of each PBXNativeTarget in the other project file. ProjectRef is
- a PBXFileReference to the other project file.
- If this project file already references the other project file, the
- existing ProductGroup and ProjectRef are returned. The ProductGroup will
- still be updated if necessary.
- """
- if "projectReferences" not in self._properties:
- self._properties["projectReferences"] = []
- product_group = None
- project_ref = None
- if other_pbxproject not in self._other_pbxprojects:
- # This project file isn't yet linked to the other one. Establish the
- # link.
- product_group = PBXGroup({"name": "Products"})
- # ProductGroup is strong.
- product_group.parent = self
- # There's nothing unique about this PBXGroup, and if left alone, it will
- # wind up with the same set of hashables as all other PBXGroup objects
- # owned by the projectReferences list. Add the hashables of the
- # remote PBXProject that it's related to.
- product_group._hashables.extend(other_pbxproject.Hashables())
- # The other project reports its path as relative to the same directory
- # that this project's path is relative to. The other project's path
- # is not necessarily already relative to this project. Figure out the
- # pathname that this project needs to use to refer to the other one.
- this_path = posixpath.dirname(self.Path())
- projectDirPath = self.GetProperty("projectDirPath")
- if projectDirPath:
- if posixpath.isabs(projectDirPath[0]):
- this_path = projectDirPath
- else:
- this_path = posixpath.join(this_path, projectDirPath)
- other_path = gyp.common.RelativePath(other_pbxproject.Path(), this_path)
- # ProjectRef is weak (it's owned by the mainGroup hierarchy).
- project_ref = PBXFileReference(
- {
- "lastKnownFileType": "wrapper.pb-project",
- "path": other_path,
- "sourceTree": "SOURCE_ROOT",
- }
- )
- self.ProjectsGroup().AppendChild(project_ref)
- ref_dict = {"ProductGroup": product_group, "ProjectRef": project_ref}
- self._other_pbxprojects[other_pbxproject] = ref_dict
- self.AppendProperty("projectReferences", ref_dict)
- # Xcode seems to sort this list case-insensitively
- self._properties["projectReferences"] = sorted(
- self._properties["projectReferences"],
- key=lambda x: x["ProjectRef"].Name().lower
- )
- else:
- # The link already exists. Pull out the relevnt data.
- project_ref_dict = self._other_pbxprojects[other_pbxproject]
- product_group = project_ref_dict["ProductGroup"]
- project_ref = project_ref_dict["ProjectRef"]
- self._SetUpProductReferences(other_pbxproject, product_group, project_ref)
- inherit_unique_symroot = self._AllSymrootsUnique(other_pbxproject, False)
- targets = other_pbxproject.GetProperty("targets")
- if all(self._AllSymrootsUnique(t, inherit_unique_symroot) for t in targets):
- dir_path = project_ref._properties["path"]
- product_group._hashables.extend(dir_path)
- return [product_group, project_ref]
- def _AllSymrootsUnique(self, target, inherit_unique_symroot):
- # Returns True if all configurations have a unique 'SYMROOT' attribute.
- # The value of inherit_unique_symroot decides, if a configuration is assumed
- # to inherit a unique 'SYMROOT' attribute from its parent, if it doesn't
- # define an explicit value for 'SYMROOT'.
- symroots = self._DefinedSymroots(target)
- for s in self._DefinedSymroots(target):
- if (
- s is not None
- and not self._IsUniqueSymrootForTarget(s)
- or s is None
- and not inherit_unique_symroot
- ):
- return False
- return True if symroots else inherit_unique_symroot
- def _DefinedSymroots(self, target):
- # Returns all values for the 'SYMROOT' attribute defined in all
- # configurations for this target. If any configuration doesn't define the
- # 'SYMROOT' attribute, None is added to the returned set. If all
- # configurations don't define the 'SYMROOT' attribute, an empty set is
- # returned.
- config_list = target.GetProperty("buildConfigurationList")
- symroots = set()
- for config in config_list.GetProperty("buildConfigurations"):
- setting = config.GetProperty("buildSettings")
- if "SYMROOT" in setting:
- symroots.add(setting["SYMROOT"])
- else:
- symroots.add(None)
- if len(symroots) == 1 and None in symroots:
- return set()
- return symroots
- def _IsUniqueSymrootForTarget(self, symroot):
- # This method returns True if all configurations in target contain a
- # 'SYMROOT' attribute that is unique for the given target. A value is
- # unique, if the Xcode macro '$SRCROOT' appears in it in any form.
- uniquifier = ["$SRCROOT", "$(SRCROOT)"]
- if any(x in symroot for x in uniquifier):
- return True
- return False
- def _SetUpProductReferences(self, other_pbxproject, product_group, project_ref):
- # TODO(mark): This only adds references to products in other_pbxproject
- # when they don't exist in this pbxproject. Perhaps it should also
- # remove references from this pbxproject that are no longer present in
- # other_pbxproject. Perhaps it should update various properties if they
- # change.
- for target in other_pbxproject._properties["targets"]:
- if not isinstance(target, PBXNativeTarget):
- continue
- other_fileref = target._properties["productReference"]
- if product_group.GetChildByRemoteObject(other_fileref) is None:
- # Xcode sets remoteInfo to the name of the target and not the name
- # of its product, despite this proxy being a reference to the product.
- container_item = PBXContainerItemProxy(
- {
- "containerPortal": project_ref,
- "proxyType": 2,
- "remoteGlobalIDString": other_fileref,
- "remoteInfo": target.Name(),
- }
- )
- # TODO(mark): Does sourceTree get copied straight over from the other
- # project? Can the other project ever have lastKnownFileType here
- # instead of explicitFileType? (Use it if so?) Can path ever be
- # unset? (I don't think so.) Can other_fileref have name set, and
- # does it impact the PBXReferenceProxy if so? These are the questions
- # that perhaps will be answered one day.
- reference_proxy = PBXReferenceProxy(
- {
- "fileType": other_fileref._properties["explicitFileType"],
- "path": other_fileref._properties["path"],
- "sourceTree": other_fileref._properties["sourceTree"],
- "remoteRef": container_item,
- }
- )
- product_group.AppendChild(reference_proxy)
- def SortRemoteProductReferences(self):
- # For each remote project file, sort the associated ProductGroup in the
- # same order that the targets are sorted in the remote project file. This
- # is the sort order used by Xcode.
- def CompareProducts(x, y, remote_products):
- # x and y are PBXReferenceProxy objects. Go through their associated
- # PBXContainerItem to get the remote PBXFileReference, which will be
- # present in the remote_products list.
- x_remote = x._properties["remoteRef"]._properties["remoteGlobalIDString"]
- y_remote = y._properties["remoteRef"]._properties["remoteGlobalIDString"]
- x_index = remote_products.index(x_remote)
- y_index = remote_products.index(y_remote)
- # Use the order of each remote PBXFileReference in remote_products to
- # determine the sort order.
- return cmp(x_index, y_index)
- for other_pbxproject, ref_dict in self._other_pbxprojects.items():
- # Build up a list of products in the remote project file, ordered the
- # same as the targets that produce them.
- remote_products = []
- for target in other_pbxproject._properties["targets"]:
- if not isinstance(target, PBXNativeTarget):
- continue
- remote_products.append(target._properties["productReference"])
- # Sort the PBXReferenceProxy children according to the list of remote
- # products.
- product_group = ref_dict["ProductGroup"]
- product_group._properties["children"] = sorted(
- product_group._properties["children"],
- key=cmp_to_key(
- lambda x, y, rp=remote_products: CompareProducts(x, y, rp)),
- )
- class XCProjectFile(XCObject):
- _schema = XCObject._schema.copy()
- _schema.update(
- {
- "archiveVersion": [0, int, 0, 1, 1],
- "classes": [0, dict, 0, 1, {}],
- "objectVersion": [0, int, 0, 1, 46],
- "rootObject": [0, PBXProject, 1, 1],
- }
- )
- def ComputeIDs(self, recursive=True, overwrite=True, hash=None):
- # Although XCProjectFile is implemented here as an XCObject, it's not a
- # proper object in the Xcode sense, and it certainly doesn't have its own
- # ID. Pass through an attempt to update IDs to the real root object.
- if recursive:
- self._properties["rootObject"].ComputeIDs(recursive, overwrite, hash)
- def Print(self, file=sys.stdout):
- self.VerifyHasRequiredProperties()
- # Add the special "objects" property, which will be caught and handled
- # separately during printing. This structure allows a fairly standard
- # loop do the normal printing.
- self._properties["objects"] = {}
- self._XCPrint(file, 0, "// !$*UTF8*$!\n")
- if self._should_print_single_line:
- self._XCPrint(file, 0, "{ ")
- else:
- self._XCPrint(file, 0, "{\n")
- for property, value in sorted(
- self._properties.items()
- ):
- if property == "objects":
- self._PrintObjects(file)
- else:
- self._XCKVPrint(file, 1, property, value)
- self._XCPrint(file, 0, "}\n")
- del self._properties["objects"]
- def _PrintObjects(self, file):
- if self._should_print_single_line:
- self._XCPrint(file, 0, "objects = {")
- else:
- self._XCPrint(file, 1, "objects = {\n")
- objects_by_class = {}
- for object in self.Descendants():
- if object == self:
- continue
- class_name = object.__class__.__name__
- if class_name not in objects_by_class:
- objects_by_class[class_name] = []
- objects_by_class[class_name].append(object)
- for class_name in sorted(objects_by_class):
- self._XCPrint(file, 0, "\n")
- self._XCPrint(file, 0, "/* Begin " + class_name + " section */\n")
- for object in sorted(
- objects_by_class[class_name], key=attrgetter("id")
- ):
- object.Print(file)
- self._XCPrint(file, 0, "/* End " + class_name + " section */\n")
- if self._should_print_single_line:
- self._XCPrint(file, 0, "}; ")
- else:
- self._XCPrint(file, 1, "};\n")
|