#!/usr/bin/env python """Ninja toolchain abstraction for XCode toolchain""" import os import subprocess import toolchain import syntax def make_target(toolchain, host, target): return XCode(toolchain, host, target) class XCode(object): def __init__(self, toolchain, host, target): self.toolchain = toolchain self.host = host self.target = target def initialize_toolchain(self): self.organisation = '' self.bundleidentifier = '' self.provisioning = '' if self.target.is_macos(): self.deploymenttarget = '12.0' elif self.target.is_ios(): self.deploymenttarget = '15.0' def build_toolchain(self): if self.target.is_macos(): sdk = 'macosx' deploytarget = 'MACOSX_DEPLOYMENT_TARGET=' + self.deploymenttarget elif self.target.is_ios(): sdk = 'iphoneos' deploytarget = 'IPHONEOS_DEPLOYMENT_TARGET=' + self.deploymenttarget platformpath = toolchain.check_last_output(['xcrun', '--sdk', sdk, '--show-sdk-platform-path']) localpath = platformpath + "/Developer/usr/bin:/Applications/Xcode.app/Contents/Developer/usr/bin:/usr/bin:/bin:/usr/sbin:/sbin" self.plist = "PATH=" + localpath + " " + toolchain.check_last_output(['xcrun', '--sdk', sdk, '-f', 'plutil']) self.xcassets = "PATH=" + localpath + " " + toolchain.check_last_output(['xcrun', '--sdk', sdk, '-f', 'actool']) self.xib = "PATH=" + localpath + " " + toolchain.check_last_output(['xcrun', '--sdk', sdk, '-f', 'ibtool']) self.dsymutil = "PATH=" + localpath + " " + toolchain.check_last_output(['xcrun', '--sdk', sdk, '-f', 'dsymutil']) self.plistcmd = 'build/ninja/plist.py --exename $exename --prodname $prodname --bundle $bundleidentifier --target $target --deploymenttarget $deploymenttarget --output $outpath $in' if self.target.is_macos(): self.xcassetscmd = 'mkdir -p $outpath && $xcassets --output-format human-readable-text --output-partial-info-plist $outplist' \ ' --app-icon AppIcon --launch-image LaunchImage --platform macosx --minimum-deployment-target ' + self.deploymenttarget + \ ' --target-device mac --compress-pngs --compile $outpath $in >/dev/null' self.xibcmd = '$xib --target-device mac --module $module --minimum-deployment-target ' + self.deploymenttarget + \ ' --output-partial-info-plist $outplist --auto-activate-custom-fonts' \ ' --output-format human-readable-text --compile $outpath $in' elif self.target.is_ios(): self.xcassetscmd = 'mkdir -p $outpath && $xcassets --output-format human-readable-text --output-partial-info-plist $outplist' \ ' --app-icon AppIcon --launch-image LaunchImage --platform iphoneos --minimum-deployment-target ' + self.deploymenttarget + \ ' --target-device iphone --target-device ipad --compress-pngs --compile $outpath $in >/dev/null' self.xibcmd = '$xib --target-device iphone --target-device ipad --module $module --minimum-deployment-target ' + self.deploymenttarget + \ ' --output-partial-info-plist $outplist --auto-activate-custom-fonts' \ ' --output-format human-readable-text --compile $outpath $in &> /dev/null ' self.dsymutilcmd = '$dsymutil $in -o $outpath' self.codesigncmd = 'build/ninja/codesign.py --target $target --prefs codesign.json --builddir $builddir --binname $binname --config $config --entitlements $entitlements $outpath' def parse_default_variables(self, variables): if not variables: return if isinstance(variables, dict): iterator = iter(variables.items()) else: iterator = iter(variables) for key, val in iterator: if key == 'deploymenttarget': self.deploymenttarget = val if key == 'organisation': self.organisation = val if key == 'bundleidentifier': self.bundleidentifier = val if key == 'provisioning': self.provisioning = val def parse_prefs(self, prefs): if self.target.is_ios() and 'ios' in prefs: iosprefs = prefs['ios'] if 'deploymenttarget' in iosprefs: self.deploymenttarget = iosprefs['deploymenttarget'] if 'organisation' in iosprefs: self.organisation = iosprefs['organisation'] if 'bundleidentifier' in iosprefs: self.bundleidentifier = iosprefs['bundleidentifier'] if 'provisioning' in iosprefs: self.provisioning = iosprefs['provisioning'] elif self.target.is_macos() and 'macos' in prefs: macosprefs = prefs['macos'] if 'deploymenttarget' in macosprefs: self.deploymenttarget = macosprefs['deploymenttarget'] if 'organisation' in macosprefs: self.organisation = macosprefs['organisation'] if 'bundleidentifier' in macosprefs: self.bundleidentifier = macosprefs['bundleidentifier'] if 'provisioning' in macosprefs: self.provisioning = macosprefs['provisioning'] def write_variables(self, writer): writer.variable('plist', self.plist) writer.variable('xcassets', self.xcassets) writer.variable('xib', self.xib) writer.variable('dsymutil', self.dsymutil) writer.variable('bundleidentifier', syntax.escape(self.bundleidentifier)) writer.variable('deploymenttarget', self.deploymenttarget) writer.variable('entitlements', 'none') def write_rules(self, writer): writer.rule('dsymutil', command = self.dsymutilcmd, description = 'DSYMUTIL $outpath') writer.rule('plist', command = self.plistcmd, description = 'PLIST $outpath') writer.rule('xcassets', command = self.xcassetscmd, description = 'XCASSETS $outpath') writer.rule('xib', command = self.xibcmd, description = 'XIB $outpath') writer.rule('codesign', command = self.codesigncmd, description = 'CODESIGN $outpath') def make_bundleidentifier(self, binname): return self.bundleidentifier.replace('$(binname)', binname) def app(self, toolchain, writer, module, archbins, outpath, binname, basepath, config, implicit_deps, resources, codesign): #Outputs builtbin = [] builtres = [] builtsym = [] #Paths builddir = os.path.join('$buildpath', config, 'app', binname) configpath = os.path.join(outpath, config) apppath = os.path.join(configpath, binname + '.app') dsympath = os.path.join(outpath, config, binname + '.dSYM') #Extract debug symbols from universal binary dsymcontentpath = os.path.join(dsympath, 'Contents') builtsym = writer.build([os.path.join(dsymcontentpath, 'Resources', 'DWARF', binname), os.path.join(dsymcontentpath, 'Resources', 'DWARF' ), os.path.join(dsymcontentpath, 'Resources'), os.path.join(dsymcontentpath, 'Info.plist'), dsymcontentpath, dsympath], 'dsymutil', archbins[config], variables = [('outpath', dsympath)]) #Copy final universal binary if self.target.is_ios(): builtbin = toolchain.copy(writer, archbins[config], os.path.join(apppath, toolchain.binprefix + binname + toolchain.binext)) else: builtbin = toolchain.copy(writer, archbins[config], os.path.join(apppath, 'Contents', 'MacOS', toolchain.binprefix + binname + toolchain.binext)) #Build resources if resources: has_resources = False #Lists of input plists and partial plist files produced by resources plists = [] assetsplists = [] xibplists = [] entitlements = [] #All resource output files outfiles = [] #First build everything except plist inputs for resource in resources: if resource.endswith('.xcassets'): if self.target.is_macos(): assetsvars = [('outpath', os.path.join(os.getcwd(), apppath, 'Contents', 'Resources'))] else: assetsvars = [('outpath', apppath)] outplist = os.path.join(os.getcwd(), builddir, os.path.splitext(os.path.basename(resource))[0] + '-xcassets.plist') assetsvars += [('outplist', outplist)] outfiles = [outplist] if self.target.is_macos(): outfiles += [os.path.join(os.getcwd(), apppath, 'Contents', 'Resources', 'AppIcon.icns')] elif self.target.is_ios(): pass #TODO: Need to list all icon and launch image files here assetsplists += writer.build(outfiles, 'xcassets', os.path.join(os.getcwd(), basepath, module, resource), variables = assetsvars) has_resources = True elif resource.endswith('.xib'): xibmodule = binname.replace('-', '_').replace('.', '_') if self.target.is_macos(): nibpath = os.path.join(apppath, 'Contents', 'Resources', os.path.splitext(os.path.basename(resource))[0] + '.nib') else: nibpath = os.path.join(apppath, os.path.splitext(os.path.basename(resource))[0] + '.nib') plistpath = os.path.join(builddir, os.path.splitext(os.path.basename(resource))[0] + '-xib.plist') xibplists += [plistpath] outfiles = [] if self.target.is_ios(): outfiles += [os.path.join(nibpath, 'objects.nib'), os.path.join(nibpath, 'objects-8.0+.nib'), os.path.join(nibpath, 'runtime.nib')] outfiles += [nibpath, plistpath] builtres += writer.build(outfiles, 'xib', os.path.join(basepath, module, resource), variables = [('outpath', nibpath), ('outplist', plistpath), ('module', xibmodule)]) has_resources = True elif resource.endswith('.plist'): plists += [os.path.join(basepath, module, resource)] elif resource.endswith('.entitlements'): entitlements += [os.path.join(basepath, module, resource)] #Extra output files/directories outfiles = [] if has_resources and self.target.is_macos(): outfiles += [os.path.join(apppath, 'Contents', 'Resources')] #Now build input plists appending partial plists created by previous resources if self.target.is_macos(): plistpath = os.path.join(apppath, 'Contents', 'Info.plist') pkginfopath = os.path.join(apppath, 'Contents', 'PkgInfo') else: plistpath = os.path.join(apppath, 'Info.plist') pkginfopath = os.path.join(apppath, 'PkgInfo') plistvars = [('exename', binname), ('prodname', binname), ('outpath', plistpath)] bundleidentifier = self.make_bundleidentifier(binname) if bundleidentifier != '': plistvars += [('bundleidentifier', bundleidentifier)] outfiles += [plistpath, pkginfopath] builtres += writer.build(outfiles, 'plist', plists + assetsplists + xibplists, implicit = [os.path.join( 'build', 'ninja', 'plist.py')], variables = plistvars) #Do code signing (might modify binary, but does not matter, nothing should have final binary as input anyway) if codesign: codesignvars = [('builddir', builddir), ('binname', binname), ('outpath', apppath), ('config', config)] if self.target.is_ios(): if self.provisioning != '': codesignvars += [('provisioning', self.provisioning)] writer.build([os.path.join(apppath, '_CodeSignature', 'CodeResources'), os.path.join(apppath, '_CodeSignature'), apppath], 'codesign', builtbin, implicit = builtres + [os.path.join('build', 'ninja', 'codesign.py')], variables = codesignvars) elif self.target.is_macos(): if self.provisioning != '': codesignvars += [('provisioning', self.provisioning)] if len(entitlements) > 0: codesignvars += [('entitlements', entitlements[0])] writer.build([os.path.join(apppath, 'Contents', '_CodeSignature', 'CodeResources'), os.path.join(apppath, 'Contents', '_CodeSignature'), os.path.join(apppath, 'Contents'), apppath], 'codesign', builtbin, implicit = builtres + [os.path.join('build', 'ninja', 'codesign.py')], variables = codesignvars) return builtbin + builtsym + builtres