Everything starts when Xcode loads your plugin bundle, just after you launch it. Xcode looks at
the content of the Info.plist
file to initialize your plugin, and read all
*.pb*spec
or .xcspec
files of your plugin Resources
folder.
At this time, Xcode don't execute any code, except if you add a NSPrincipalClass
entry
in the Info.plist
file, it just stores informations about what provide your plugin.
The second phase, the most important one, is the dependency graph creation. It appends every time you open a project, add/remove files to/from a project, modify some build settings… It's the phase where your plugin code is run.
Your code must create one XCDependencyNode
for each file involved in the compilation
(source file, object files, final or product files), and add dependency relations between each nodes.
Some of these nodes will be marked as "product nodes" by calling addProductNode:
, nodes
representing a file of your final product. Nodes are identified by a name which is, by convention,
the POSIX path of the corresponding file.
Here is the description of how is run the dependency creation phase�:
PBXTargetBuildContext
object. This object will
be passed as parameter to all your fonctions. You must use it to modify/expand environment
variables, modify the dependency graph and create commands..trgttmpl
file (in
Library/Application�Support/Apple/Developer�Tools/Target�Templates/
) to the project..pbprodspec
and .pbpackspec
files. I did not really understood the differences between these two specification types, except
that they are strongly related.XCProductTypeSpecification
class, Xcode calls
it's computeProductDependencies…:
method�: it must contains code to
define product nodes..buildrules
files, or created by the user in
the "Rules" tab of the target information window) to find which compiler must be used to compile
the source file. Xcode now call for each source file the computeDependenciesForFilePath…
method of the appropriated compiler (an object of a class inherited from
XCCompilerSpecification
). The code of this method must create dependency links
between source and compiled file, add the compiled file to the object_files_$(variant)_$(arch)
environment variable, create the command which will be used to compile the source file, and
return the list of output nodes created by this command.linkerSpecificationForObjectFiles…
method of the product
specification object (inherited from the XCProductTypeSpecification
class). If a
linker is specified by the compiler_mandated_linker
variable, Xcode use it instead of
the linker returned by linkerSpecificationForObjectFiles…
. You may also set
the key RequiredLinker
in a compiler specification to override the value defined by
the product specification. Then, Xcode calls the computeDependenciesForFilePaths…
of this linker�: this method must create dependency links between product nodes and object files,
the command to be runned to call the linker, and return the list of files created by the linker.The last phase is the real compilation/linking of files, the building phase. Here, Xcode read the dependency graph and run the command only if they are needed (i.e. if the output file doesn't exist or if the input files have been modified since last compilation) and in an best order that satisfy the dependencies.
(only for Xcode 2.1) During the building phase, Xcode create .dot
files describing the dependency graph
after and before running the commands (you'll find them in build/�project�.build/�configuration�/�target�.build/
).
You need the Graphviz application to view them.
These .dot files are very useful to check if your code create the good dependency graph.
Xcode methods called by your plugin�:
-[PBXTargetBuildContext�dependencyNodeForName:createIfNeeded:]
-[PBXTargetBuildContext�addProductNode:]
-[PBXTargetBuildContext�createCommandWithRuleInfo:commandPath:arguments:forNode:]
-[XCDependencyNode�addDependedNode:]
(must be called after creating a dependency
command for the output file) -[XCDependencyNode�addIncludeNode:]
(only for included header files)-[PBXTargetBuildContext�setCompiledFilePath:forSourceFilePath:]
Methods of your plugin called by Xcode�:
-[MyProductTypeSpecification�computeProductDependenciesInTargetBuildContext:]
-[MyProductTypeSpecification�linkerSpecificationForObjectFilesInTargetBuildContext:]
-[MyCompilerSpecification�computeDependenciesForFilePath:ofType:outputDirectory:inTargetBuildContext:]
-[MyLinkerSpecification�computeDependenciesForFilePaths:outputPath:inTargetBuildContext:]
For more informations, see the XCPSpecifications.h
, XCPBuildSystem.h
and
XCPDependencyGraph.h
header files available here.
.pbcompspec
)I've never tried this solution myself, and, according to people trying to use it, it doesn't work. It's here for the case where you've more chance than others ;).
Starting with Xcode version 2.3, it's possible to integrate a compiler, without writing any code.
All you have to do is to write a mycompiler.pbcompspec
file with the following format�:
{ Identifier = com.domain.me.compiler; Class = MyCompilerSpecification; // optional CommandInvocationClass = MyToolInvocation; // optional Name = "My Compiler"; Description = "My source compiler"; Version = "Default"; // optional Vendor = "Me"; // optional Architectures = ("ppc","i386"); // optional IsArchitectureNeutral = No; // optional (if "Yes", "Architectures" is useless) Languages = (mytype); // optional FileTypes = (sourcecode.mytype); ExecPath = "/usr/local/bin/mycompiler"; RuleName = ( "ProcessingXXX", "$(OutputFile)", "$(InputFile)" ); OutputFileSuffix = ".o"; RequiredLinker = com.domain.me.linker; // optional (linker that must be used if using this // compiler for at least one source file) RequiredLibrarian = ??; // optional CommandOutputParser = (…); // optional PatternsOfFlagsNotAffectingPrecomps = (…) // optional OptionsForCommandLine = (…); // optional Options = { { Name = Input; Type = stringlist; CommandLineArgs = ( "$(value)" ); InputDependencies = "$(Input)"; // add to list of files needed before running the compiler IsCommandInput = YES; }, { Name = Output; Type = stringlist; CommandLineArgs = ( "-o", "$(value)" ); OutputDependencies = "$(Output)"; // add to list of files created by the compiler IsCommandOutput = YES; }, { Name = "build_file_compiler_flags"; // compiler flags specific to this source file Type = stringlist; CommandLineArgs = { "" = (); "<<otherwise>>" = ( "$(value)" ); }; } … // see here for more details on options }; OptionCategories = {…}; // optional (see here) }
As you can see, Xcode creates to new environment variables $(OutputFile)
and
$(InputFile)
. The output file path is derived from the input path by replacing the
extension with the value of the OutputFileSuffix
key.
If the first solution don't allow you to do what you want, you need to write a custom compiler
specification class (inheriting from XCCompilerSpecification
). In this case you need
to add the Class
key, but RuleName
, OutputFileSuffix
,
OutputFileSuffix
are not required. You also don't need to use the InputFile
,
OutputFile
and build_file_compiler_flags
options.
Code file MyCompilerSpecification.m
�:
#import "MyCompilerSpecification.h" #import "XCPBuildSystem.h" #import "XCPDependencyGraph.h" #import "XCPSupport.h" @implementation MyCompilerSpecification - (NSArray*) computeDependenciesForInputFile:(NSString*)input ofType:(PBXFileType*)type variant:(NSString*)variant architecture:(NSString*)arch outputDirectory:(NSString*)outputDir inTargetBuildContext:(PBXTargetBuildContext*)context { // compute input path (for variable substitution) input = [context expandedValueForString:input]; // compute output path NSString* output = [outputDir stringByAppendingPathComponent:[[input lastPathComponent] stringByDeletingPathExtension]]; output = [context expandedValueForString:output]; // create dependency nodes XCDependencyNode* outputNode = [context dependencyNodeForName:output createIfNeeded:YES]; XCDependencyNode* inputNode = [context dependencyNodeForName:input createIfNeeded:YES]; // create compiler command XCDependencyCommand* dep = [context createCommandWithRuleInfo:[NSArray arrayWithObjects:@"MyCompile",[context naturalPathForPath:input],nil] commandPath:[context expandedValueForString:[self path]] arguments:nil forNode:outputNode]; [dep setToolSpecification:self]; [dep addArgumentsFromArray:[self commandLineForAutogeneratedOptionsInTargetBuildContext:context]]; [dep addArgumentsFromArray:[[context expandedValueForString:@"$(build_file_compiler_flags)"] arrayByParsingAsStringList]]; [dep addArgument:@"-o"]; [dep addArgument:output]; [dep addArgument:input]; // create dependency rules [outputNode addDependedNode:inputNode]; // update source-compiled links [context setCompiledFilePath:output forSourceFilePath:input]; // add to the list of file for the linker (only for non-".o" objects) //NSString* object_files_variant_arch = [context expandedValueForString:@"object_files"]; //[context appendStringOrStringListValue:output toDynamicSetting:object_files_variant_arch]; // return output object node (Xcode will automaticaly add .o files to $(object_files)) return [NSArray arrayWithObject:outputNode]; } @end
.pblinkspec
)Specification file mylinker.pblinkspec
�:
{ Identifier = com.domain.me.linker; Class = MyLinkerSpecification; Name = "My linker"; Description = "My linker"; Version = "Default"; // optional Vendor = "Me"; // optional BinaryFormats = ("mach-o"); Architectures = (ppc); ExecPath = "/usr/local/bin/mylinker"; InputFileTypes = (compiled.mach-o.objfile); CommandOutputParser = (…); PatternsOfFlagsNotAffectingPrecomps = (…) // optional OptionsForCommandLine = (…); // optional (see here) Options = {…}; // optional (see here) OptionCategories = {…}; // optional (see here) }
Code file MyLinkerSpecification.m
�:
#import "MyLinkerSpecification.h" #import "XCPBuildSystem.h" #import "XCPDependencyGraph.h" @implementation MyLinkerSpecification - (NSArray*)createCommandsInBuildContext:(PBXTargetBuildContext*)context { // this method must be override since Xcode 3.0 : the default implementation doesn't call // computeDependenciesForFilePaths: anymore NSArray* inputs = [[context expandedValueForString:@"$(Inputs)"] arrayByParsingAsStringList]; NSString* output = [context expandedValueForString:@"$(Output)"]; return [self computeDependenciesForFilePaths:inputs outputPath:output inTargetBuildContext:context]; } - (NSArray*)computeDependenciesForFilePaths:(NSArray*)inputs outputPath:(NSString*)output inTargetBuildContext:(PBXTargetBuildContext*)context { // compute output path (for variable substitution) output = [context expandedValueForString:output]; // create linker command XCDependencyNode* outputNode = [context dependencyNodeForName:output createIfNeeded:YES]; XCDependencyCommand* dep = [context createCommandWithRuleInfo:[NSArray arrayWithObjects:@"MyLink",[context naturalPathForPath:output],nil] commandPath:[context expandedValueForString:[self path]] arguments:nil forNode:outputNode]; [dep setToolSpecification:self]; [dep addArgumentsFromArray:[self commandLineForAutogeneratedOptionsInTargetBuildContext:context]]; [dep addArgument:@"-o"]; [dep addArgument:output]; // some types PBXFileType* myObjectFileType = [PBXFileType specificationForIdentifier:@"compiled.myobjfile"]; PBXFileType* myLibraryFileType = [PBXFileType specificationForIdentifier:@"compiled.mylibraryfile"]; // create dependency rules & command arguments for libraries NSEnumerator* libraryEnum = [[context linkedLibraryPaths] objectEnumerator]; NSString* library; while((library = [libraryEnum nextObject]) != nil) { library = [context expandedValueForString:library]; PBXFileType* type = [PBXFileType fileTypeForFileName:[library lastPathComponent]]; if([type isKindOfSpecification:myLibraryFileType] || [type isKindOfSpecification:myObjectFileType]) { XCDependencyNode* libraryNode = [context dependencyNodeForName:library createIfNeeded:YES]; [outputNode addDependedNode:libraryNode]; [dep addArgument:library]; } else { [context addDependencyAnalysisWarningMessageFormat: @"warning: skipping file '%@' (unexpected file type '%@' in Frameworks & Libraries build phase)", library, [type identifier]]; } } // create dependency rules & command arguments for compiled object NSEnumerator* objectEnum = [inputs objectEnumerator]; NSString* input; while((input = [objectEnum nextObject]) != nil) { input = [context expandedValueForString:input]; PBXFileType* type = [PBXFileType fileTypeForFileName:[input lastPathComponent]]; if([type isKindOfSpecification:myObjectFileType]) { XCDependencyNode* inputNode = [context dependencyNodeForName:input createIfNeeded:YES]; [outputNode addDependedNode:inputNode]; [dep addArgument:input]; } else { [context addDependencyAnalysisWarningMessageFormat: @"warning: skipping file '%@' (unexpected file type '%@' in link phase)", input, [type identifier]]; } } return [NSArray arrayWithObject:outputNode]; } @end
TODO
.pbprodspec
)Specification file myproducttype.pbprodspec
�:
{ Identifier = com.domain.me.product-type.tool; Class = MyProductTypeSpecification; Name = "My Command-line Tool"; Description = "My Command-line Tool"; //Image name for inactive target is <IconNamePrefix> //Image name for active target is <IconNamePrefix>Active IconNamePrefix = "TargetPlugin"; DefaultTargetName = "MyTool"; DefaultBuildProperties = { … }; AllowedBuildPhaseTypes = ( Headers, Sources, Frameworks ); BuildPropertiesToAppend = ??; PackageTypes = ( com.domain.me.package-type.tool ); }
Code file MyProductTypeSpecification.m
�:
#import "MyProductTypeSpecification.h" #import "XCPBuildSystem.h" @implementation MyProductTypeSpecification - (void)computeProductDependenciesInTargetBuildContext:(PBXTargetBuildContext*)context { NSString* productPath = [context expandedValueForString:@"$(TARGET_BUILD_DIR)/$(EXECUTABLE_PATH)"]; XCDependencyNode* productNode = [context dependencyNodeForName:productPath createIfNeeded:YES]; [context addProductNode:productNode]; } - (XCLinkerSpecification*)linkerSpecificationForObjectFilesInTargetBuildContext:(PBXTargetBuildContext*)context { return [[XCLinkerSpecification specificationRegistry] objectForKey:@"com.domain.me.linker"]; } @end