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