Xcode's Plugin Interface : Compilers & Linkers

Table of Contents

UP (Xcode's Plugin Interface)
1. Xcode's Build System
2. Graph Creation API
3. Compiler Sample Code (.pbcompspec)
4. Linker Sample Code (.pblinkspec)
5. Parsing the ouput of command line tools (TODO)
6. Product Type Sample Code (.pbprodspec)

1. Xcode's Build System

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�:

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.

2. Graph Creation API

Xcode methods called by your plugin�:

Methods of your plugin called by Xcode�:

For more informations, see the XCPSpecifications.h, XCPBuildSystem.h and XCPDependencyGraph.h header files available here.

3. Compiler Sample Code (.pbcompspec)

First solution : no code, less generic

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.

Second solution : with Objective-C code, more generic

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

4. Linker Sample Code (.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

5. Parsing the ouput of command line tools

TODO

6. Product Type Sample Code (.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