/* MakeBuilder.m Implementation of the MakeBuilder class for the ProjectManager application. Copyright (C) 2005 Saso Kiselkov This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA */ #import "MakeBuilder.h" #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import "MakeBuilderDelegate.h" #import "../../ProjectDocument.h" #import "../../NSImageAdditions.h" #import "OSType.h" /** * Finds out whether a specified search string is contained inside another * string. * * @param string The string which to search. * @param search The string for which to look. * * @return YES if the searched string is inside `string', otherwise NO. */ static inline BOOL ContainsString(NSString * string, NSString * search) { return ([string rangeOfString: search].location != NSNotFound); } /** * Produces an build error/warning-message description dictionary from * the provided arguments. * * @param string The string which to parse and construct a description of. * @param dirStack The directory stack of the builder when it received * the error/warning message. * * @return A dictionary describing the error/warning message. The dictionary * will have the following format: * { * File = "/path/to/error/file"; * Line = (NSNumber *) error-line; * Description = (NSString/NSAttributedString *) error-description; * } *. * Error messages have the 'Description' key's value colored red, * warnings yellow and in case the string could not be parsed, both * the 'File' and 'Line' keys will be left empty and the 'Description' * key will contain the entire input line uncolored. */ static NSDictionary * ErrorInfoFromString (NSString * string, NSArray * dirStack) { NSArray * components; // try splitting the line at ':' boundaries. There should be at least 4 // components, structured like this: // // ::: // or // :::: components = [string componentsSeparatedByString: @":"]; if ([components count] >= 4 && [[[components objectAtIndex: 1] stringByTrimmingCharactersInSet: [NSCharacterSet decimalDigitCharacterSet]] length] == 0) { NSString * filePath, * file, * lineString, * type, * description; NSNumber * line; unsigned int headerLength; NSColor * color; NSUserDefaults * df = [NSUserDefaults standardUserDefaults]; // append the file name to the current build directory (top element // on the build directory stack) file = [components objectAtIndex: 0]; filePath = [[dirStack lastObject] stringByAppendingPathComponent: file]; lineString = [components objectAtIndex: 1]; line = [NSNumber numberWithInt: [lineString intValue]]; // first try the preprocessor format with "line:column:message-type" // indication, switching back to the compiler style (line:message-type) // if what we read was neither "warning" nor "error" type = [[components objectAtIndex: 3] stringByTrimmingSpaces]; if (![type isEqualToString: _(@"warning")] && ![type isEqualToString: _(@"error")]) { type = [[components objectAtIndex: 2] stringByTrimmingSpaces]; } // the rest is the error description headerLength = [file length] + [lineString length] + [[components objectAtIndex: 2] length] + 3; description = [[string substringFromIndex: headerLength] stringByTrimmingSpaces]; // if the type is known (is one of "error" or "warning"), // then colorize the description. Otherwise leave it alone. if ([type caseInsensitiveCompare: @"error"] == NSOrderedSame) { color = [df objectForKey: @"ErrorColor"]; if (color == nil) color = [NSColor redColor]; description = [NSString stringWithFormat: _(@"Error: %@"), description]; } else if ([type caseInsensitiveCompare: @"warning"] == NSOrderedSame) { color = [df objectForKey: @"WarningColor"]; if (color == nil) color = [NSColor yellowColor]; description = [NSString stringWithFormat: _(@"Warning: %@"), description]; } else { color = nil; } if (color != nil) { NSDictionary * attributes = [NSDictionary dictionaryWithObject: color forKey: NSForegroundColorAttributeName]; return [NSDictionary dictionaryWithObjectsAndKeys: filePath, @"File", line, @"Line", [[[NSAttributedString alloc] initWithString: description attributes: attributes] autorelease], @"Description", nil]; } else { return [NSDictionary dictionaryWithObjectsAndKeys: filePath, @"File", line, @"Line", description, @"Description", nil]; } } else // otherwise mark this as an unknown error { return [NSDictionary dictionaryWithObject: string forKey: @"Description"]; } } @interface MakeBuilder (Private) - (BOOL) validateControl: control; - (void) setBuilderState: (MakeBuilderState) aState; - (void) flushBuildDirectoryStack; @end @implementation MakeBuilder (Private) /** * Validates the provided control. * * @param control The control to be validated. The object must respond * to an -action message. * * @return YES if the control is valid, NO otherwise. */ - (BOOL) validateControl: control { SEL action = [control action]; if (sel_eq (action, @selector (build:)) || sel_eq (action, @selector (clean:))) { return (state == MakeBuilderReady); } else if (sel_eq (action, @selector (stopOperation:))) { return (state == MakeBuilderBuilding || state == MakeBuilderCleaning); } else { return YES; } } /** * Sets up the receiver to a certain state and updates it's GUI. * * @param aState The state to which to set the receiver up. */ - (void) setBuilderState: (MakeBuilderState) aState { state = aState; switch (state) { case MakeBuilderReady: [buildTarget setEnabled: YES]; break; case MakeBuilderBuilding: [buildTarget setEnabled: NO]; break; case MakeBuilderCleaning: [buildTarget setEnabled: NO]; break; } [self flushBuildDirectoryStack]; [[[view window] toolbar] validateVisibleItems]; } - (void) flushBuildDirectoryStack { int n = [buildDirectoryStack count]; while (n > 1) { [buildDirectoryStack removeLastObject]; } } @end @implementation MakeBuilder + (NSString *) moduleName { return @"MakeBuilder"; } + (NSString *) humanReadableModuleName { return _(@"Builder"); } - (NSArray *) moduleMenuItems { return [NSArray arrayWithObjects: PMMakeMenuItem (_(@"Build"), @selector(build:), @"B", self), PMMakeMenuItem (_(@"Clean"), @selector(clean:), @"C", self), PMMakeMenuItem (_(@"Stop"), @selector(stopOperation:), nil, self), nil]; } - (NSArray *) toolbarItemIdentifiers { return [NSArray arrayWithObjects: @"MakeBuilderBuildItemIdentifier", @"MakeBuilderCleanItemIdentifier", @"MakeBuilderStopItemIdentifier", nil]; } - (NSToolbarItem *) toolbarItemForItemIdentifier: (NSString *) identifier { NSToolbarItem * item = [[[NSToolbarItem alloc] initWithItemIdentifier: identifier] autorelease]; [item setTarget: self]; if ([identifier isEqualToString: @"MakeBuilderBuildItemIdentifier"]) { [item setImage: [NSImage imageNamed: @"Build" owner: self]]; [item setAction: @selector(build:)]; [item setLabel: _(@"Build")]; [item setToolTip: _(@"Builds the selected target of the project")]; } else if ([identifier isEqualToString: @"MakeBuilderCleanItemIdentifier"]) { [item setImage: [NSImage imageNamed: @"Clean" owner: self]]; [item setAction: @selector(clean:)]; [item setLabel: _(@"Clean")]; [item setToolTip: _(@"Cleans the project")]; } else if ([identifier isEqualToString: @"MakeBuilderStopItemIdentifier"]) { [item setImage: [NSImage imageNamed: @"Stop" owner: self]]; [item setAction: @selector(stopOperation:)]; [item setLabel: _(@"Stop")]; [item setToolTip: _(@"Stops a build or clean operation")]; } return item; } - (void) dealloc { [[NSNotificationCenter defaultCenter] removeObserver: self]; TEST_RELEASE(view); TEST_RELEASE(targets); TEST_RELEASE(buildArguments); TEST_RELEASE(buildErrorList); TEST_RELEASE(lastIncompleteOutputLine); TEST_RELEASE(lastIncompleteErrorLine); TEST_RELEASE(buildDirectoryStack); if (task != nil && [task isRunning]) { [task interrupt]; } TEST_RELEASE(task); TEST_RELEASE(info); [super dealloc]; } - initWithDocument: (ProjectDocument *) aDocument infoDictionary: (NSDictionary *) infoDict { if ((self = [self init]) != nil) { document = aDocument; buildArguments = [NSMutableArray new]; [buildArguments addObjectsFromArray: [infoDict objectForKey: @"BuildArguments"]]; buildErrorList = [NSMutableArray new]; [buildErrorList addObjectsFromArray: [infoDict objectForKey: @"BuildErrors"]]; buildDirectoryStack = [NSMutableArray new]; [buildDirectoryStack addObject: [document projectDirectory]]; lastIncompleteOutputLine = [NSString new]; lastIncompleteErrorLine = [NSString new]; ASSIGNCOPY(info, infoDict); } return self; } - (ProjectDocument *) document { return document; } - (NSView *) view { if (view == nil) { [NSBundle loadNibNamed: @"MakeBuilder" owner: self]; } return view; } - (NSDictionary *) infoDictionary { return [NSDictionary dictionaryWithObjectsAndKeys: buildArguments, @"BuildArguments", [NSNumber numberWithBool: [verboseBuild state]], @"VerboseBuild", [NSNumber numberWithBool: [warnings state]], @"Warnings", [NSNumber numberWithBool: [allWarnings state]], @"AllWarnings", nil]; } - (BOOL) regenerateDerivedFiles { return YES; } - (BOOL) validateMenuItem: (id ) item { return [self validateControl: item]; } - (BOOL) validateToolbarItem: (NSToolbarItem *) item { return [self validateControl: item]; } - (void) finishInit { delegate = (id ) [document projectType]; ASSIGN(targets, [delegate buildTargetsForMakeBuilder: self]); [buildTarget removeAllItems]; [buildTarget addItemsWithTitles: targets]; if ([buildTarget numberOfItems] > 0) { [buildTarget selectItemAtIndex: 0]; } } - (void) awakeFromNib { NSButtonCell * cell; NSRect r; [view retain]; [view removeFromSuperview]; DESTROY(bogusWindow); [buildErrors setTarget: self]; [buildErrors setDoubleAction: @selector(openErrorFile:)]; [[[buildArgs enclosingScrollView] verticalScroller] setArrowsPosition: NSScrollerArrowsNone]; [self setBuilderState: MakeBuilderReady]; cell = [buildArgsMovementMatrix cellWithTag: 0]; [cell setTarget: self]; [cell setAction: @selector(moveBuildArgumentUp:)]; cell = [buildArgsMovementMatrix cellWithTag: 1]; [cell setTarget: self]; [cell setAction: @selector(moveBuildArgumentDown:)]; cell = [buildArgsManipulationMatrix cellWithTag: 0]; [cell setTarget: self]; [cell setAction: @selector(addBuildArgument:)]; cell = [buildArgsManipulationMatrix cellWithTag: 1]; [cell setTarget: self]; [cell setAction: @selector(removeBuildArgument:)]; [buildTarget removeAllItems]; [buildTarget addItemsWithTitles: targets]; if ([buildTarget numberOfItems] > 0) { [buildTarget selectItemAtIndex: 0]; } [verboseBuild setState: [[info objectForKey: @"VerboseBuild"] boolValue]]; [warnings setState: [[info objectForKey: @"Warnings"] boolValue]]; [allWarnings setState: [[info objectForKey: @"AllWarnings"] boolValue]]; } /** * Action to initiate the build process with the current target. */ - (void) build: sender { [self buildTarget: [buildTarget titleOfSelectedItem]]; } /** * Builds the specified target of the project. * * @param target The target which to build. */ - (void) buildTarget: (NSString *) target { NSNotificationCenter * nc = [NSNotificationCenter defaultCenter]; NSString * makeCommand; NSMutableArray * arguments = [NSMutableArray array]; NSAssert(state == MakeBuilderReady, _(@"Attempted to start build when " @"builder wasn't ready.")); [self setBuilderState: MakeBuilderBuilding]; [document logMessage: [NSString stringWithFormat: _(@"Building target %@"), target]]; if ([delegate prepareForBuildByBuilder: self target: target] == NO) { return; } // construct the build task ASSIGN(task, [NSTask new]); // set the build task's launch path makeCommand = [[NSUserDefaults standardUserDefaults] objectForKey: @"MakeCommand"]; if (makeCommand == nil) { #ifdef LINUX // GNU/Linux systems use GNU Make by default makeCommand = @"make"; #elif defined (FREEBSD) // FreeBSD has it named "gmake" makeCommand = @"gmake"; #else #warning "Your system type is not supported, so I don't know which make" #warning "command to use by default - assuming 'make' for now. You can" #warning "change that at run-time by setting the MakeCommand default" makeCommand = @"make"; #endif } [task setLaunchPath: makeCommand]; [task setLaunchPath: [task validatedLaunchPath]]; // build in the project path [task setCurrentDirectoryPath: [document projectDirectory]]; // construct the build task's arguments [arguments addObjectsFromArray: [delegate buildArgumentsForBuilder: self target: target]]; if ([verboseBuild state]) { [arguments addObject: @"messages=yes"]; } if ([warnings state]) { [arguments addObject: @"warnings=yes"]; } if ([allWarnings state]) { [arguments addObject: @"allwarnings=yes"]; } // append additional user-defined arguments [arguments addObjectsFromArray: buildArguments]; [task setArguments: arguments]; [nc addObserver: self selector: @selector(buildCompleted:) name: NSTaskDidTerminateNotification object: task]; // set up the stdout and stderr pipes { NSPipe * outputPipe = [NSPipe pipe], * errorPipe = [NSPipe pipe]; [task setStandardOutput: outputPipe]; [task setStandardError: errorPipe]; ASSIGN(outputFileHandle, [outputPipe fileHandleForReading]); ASSIGN(errorFileHandle, [errorPipe fileHandleForReading]); [nc addObserver: self selector: @selector(collectOutput:) name: NSFileHandleDataAvailableNotification object: outputFileHandle]; [nc addObserver: self selector: @selector(collectErrorOutput:) name: NSFileHandleDataAvailableNotification object: errorFileHandle]; } // clean the output lists [buildErrorList removeAllObjects]; [buildErrors reloadData]; [buildOutput setString: @""]; // and start the build NS_DURING [task launch]; NS_HANDLER { NSDictionary * userInfo; [self setBuilderState: MakeBuilderReady]; userInfo = [NSDictionary dictionaryWithObjectsAndKeys: document, @"Project", target, @"Target", [NSNumber numberWithBool: NO], @"Success", nil]; [nc postNotificationName: MakeBuilderBuildDidEndNotification object: self userInfo: userInfo]; NSRunAlertPanel(_(@"Failed to build"), _(@"Failed to run the build command \"%@\""), nil, nil, nil, makeCommand); return; } NS_ENDHANDLER [outputFileHandle waitForDataInBackgroundAndNotify]; [errorFileHandle waitForDataInBackgroundAndNotify]; { NSDictionary * userInfo; userInfo = [NSDictionary dictionaryWithObjectsAndKeys: document, @"Project", target, @"Target", nil]; [nc postNotificationName: MakeBuilderBuildDidBeginNotification object: self userInfo: userInfo]; } } /** * Action to initiate the clean process with the current target. */ - (void) clean: (id)sender { [self cleanTarget: [buildTarget titleOfSelectedItem]]; } /** * Cleans the specified target of the project. * * @param target The target which to clean. */ - (void) cleanTarget: (NSString *) target { NSNotificationCenter * nc = [NSNotificationCenter defaultCenter]; NSString * makeCommand; NSMutableArray * arguments = [NSMutableArray array]; NSAssert(state == MakeBuilderReady, _(@"Attempted to start clean when " @"builder wasn't ready.")); [self setBuilderState: MakeBuilderCleaning]; if ([delegate prepareForCleanByBuilder: self target: target] == NO) { return; } [document logMessage: [NSString stringWithFormat: _(@"Cleaning target %@"), target]]; // construct the clean task ASSIGN(task, [NSTask new]); // set the clean task's launch path makeCommand = [[NSUserDefaults standardUserDefaults] objectForKey: @"MakeCommand"]; if (makeCommand == nil) { makeCommand = @"make"; } [task setLaunchPath: makeCommand]; [task setLaunchPath: [task validatedLaunchPath]]; // build in the project path [task setCurrentDirectoryPath: [document projectDirectory]]; // construct the task's arguments [arguments addObjectsFromArray: [delegate cleanArgumentsForBuilder: self target: target]]; if ([verboseBuild state]) { [arguments addObject: @"messages=yes"]; } if ([warnings state]) { [arguments addObject: @"warnings=yes"]; } if ([allWarnings state]) { [arguments addObject: @"allwarnings=yes"]; } [task setArguments: arguments]; [nc addObserver: self selector: @selector(cleanCompleted:) name: NSTaskDidTerminateNotification object: task]; // set up the stdout and stderr pipes { NSPipe * outputPipe = [NSPipe pipe], * errorPipe = [NSPipe pipe]; [task setStandardOutput: outputPipe]; [task setStandardError: errorPipe]; ASSIGN(outputFileHandle, [outputPipe fileHandleForReading]); ASSIGN(errorFileHandle, [errorPipe fileHandleForReading]); [nc addObserver: self selector: @selector(collectOutput:) name: NSFileHandleDataAvailableNotification object: outputFileHandle]; [nc addObserver: self selector: @selector(collectErrorOutput:) name: NSFileHandleDataAvailableNotification object: errorFileHandle]; } // clean the output lists [buildErrorList removeAllObjects]; [buildErrors reloadData]; [buildOutput setString: @""]; // and start the clean operation NS_DURING [task launch]; NS_HANDLER { NSDictionary * userInfo; [self setBuilderState: MakeBuilderReady]; userInfo = [NSDictionary dictionaryWithObjectsAndKeys: document, @"Project", target, @"Target", [NSNumber numberWithBool: NO], @"Success", nil]; [nc postNotificationName: MakeBuilderCleanDidEndNotification object: self userInfo: userInfo]; NSRunAlertPanel(_(@"Failed to clean"), _(@"Failed to run the clean command \"%@\""), nil, nil, nil, makeCommand); return; } NS_ENDHANDLER [outputFileHandle waitForDataInBackgroundAndNotify]; [errorFileHandle waitForDataInBackgroundAndNotify]; { NSDictionary * userInfo; userInfo = [NSDictionary dictionaryWithObjectsAndKeys: document, @"Project", target, @"Target", nil]; [nc postNotificationName: MakeBuilderCleanDidBeginNotification object: self userInfo: userInfo]; } } /** * Queries whether the receiver is currently executing either a build or * clean operation (is busy). * * @return YES if the receiver is busy, NO otherwise. */ - (BOOL) isBusy { return (state != MakeBuilderReady); } /** * Action to initiate the stop a running build or clean process. */ - (void) stopOperation: sender { NSNotificationCenter * nc = [NSNotificationCenter defaultCenter]; NSDictionary * userInfo; NSString * notification; NSAssert(state == MakeBuilderBuilding || state == MakeBuilderCleaning, _(@"Attempted to stop an operation when builder was neither building " @"nor cleaning.")); [nc removeObserver: self]; [task interrupt]; DESTROY(task); DESTROY(outputFileHandle); DESTROY(errorFileHandle); userInfo = [NSDictionary dictionaryWithObjectsAndKeys: document, @"Project", [buildTarget titleOfSelectedItem], @"Target", [NSNumber numberWithBool: NO], @"Success", nil]; if (state == MakeBuilderBuilding) { notification = MakeBuilderBuildDidEndNotification; } else { notification = MakeBuilderCleanDidEndNotification; } [nc postNotificationName: notification object: self userInfo: userInfo]; [self setBuilderState: MakeBuilderReady]; } /** * Action to add a build argument. */ - (void) addBuildArgument: (id)sender { [buildArguments addObject: _(@"")]; [buildArgs reloadData]; } /** * Action to remove a build argument. */ - (void) removeBuildArgument: (id)sender { int row = [buildArgs selectedRow]; if (row >= 0) { [buildArguments removeObjectAtIndex: row]; [buildArgs reloadData]; } } /** * Action to move a build argument upwards in the list. */ - (void) moveBuildArgumentUp: sender { int row = [buildArgs selectedRow]; if (row > 0 && [buildArguments count] > 1) { [buildArguments insertObject: [buildArguments objectAtIndex: row] atIndex: row - 1]; [buildArguments removeObjectAtIndex: row + 1]; [buildArgs reloadData]; [buildArgs selectRow: row - 1 byExtendingSelection: NO]; } } /** * Action to move a build argument downwards in the list. */ - (void) moveBuildArgumentDown: sender { int row = [buildArgs selectedRow]; if (row >= 0 && row + 1 < (int) [buildArguments count] && [buildArguments count] > 1) { [buildArguments insertObject: [buildArguments objectAtIndex: row] atIndex: row + 2]; [buildArguments removeObjectAtIndex: row]; [buildArgs reloadData]; [buildArgs selectRow: row + 1 byExtendingSelection: NO]; } } /** * Action to open the error file at the specified line when the user * double-clicks an error in the build error list. */ - (void) openErrorFile: sender { id filename; int clickedRow = [buildErrors clickedRow]; unsigned int line; NSDictionary * description; if (clickedRow < 0) return; // extract the filename and line description = [buildErrorList objectAtIndex: clickedRow]; filename = [description objectForKey: @"File"]; if ([filename length] == 0) { NSRunAlertPanel(_(@"No file to open"), _(@"This error/warning message doesn't have a file " @"associated with it."), nil, nil, nil); return; } line = [[description objectForKey: @"Line"] intValue]; if (![document openFile: filename inCodeEditorOnLine: line]) { NSRunAlertPanel (_(@"Unable to open file"), _(@"Unable to open the file %@ in an editor"), nil, nil, nil, filename); } } - (int) numberOfRowsInTableView: (NSTableView *)aTableView { if (aTableView == buildArgs) { return [buildArguments count]; } else { return [buildErrorList count]; } } - (id) tableView: (NSTableView *)aTableView objectValueForTableColumn: (NSTableColumn *)aTableColumn row: (int)rowIndex { if (aTableView == buildArgs) { NSString * identifier = [aTableColumn identifier]; if ([identifier isEqualToString: @"Number"]) { return [NSNumber numberWithInt: rowIndex]; } else { return [buildArguments objectAtIndex: rowIndex]; } } else { NSString * identifier = [aTableColumn identifier]; NSString * value = [[buildErrorList objectAtIndex: rowIndex] objectForKey: identifier]; if ([identifier isEqualToString: @"File"]) { NS_DURING // strip the project directory path from it, leaving intact // any subproject directories, so the user can follow them value = [value stringByDeletingPrefix: [document projectDirectory]]; NS_HANDLER value = nil; NS_ENDHANDLER } return value; } } - (void) tableView: (NSTableView *)aTableView setObjectValue: (id)anObject forTableColumn: (NSTableColumn *)aTableColumn row: (int)rowIndex { [buildArguments replaceObjectAtIndex: rowIndex withObject: anObject]; } - (void) collectOutput: (NSNotification *) notif { NSString * string; NSMutableArray * incommingLines; string = [[[NSString alloc] initWithData: [outputFileHandle availableData] encoding: NSUTF8StringEncoding] autorelease]; incommingLines = [[[string componentsSeparatedByString: @"\n"] mutableCopy] autorelease]; [buildOutput replaceCharactersInRange: NSMakeRange([[buildOutput string] length], 0) withString: string]; [buildOutput scrollRangeToVisible: NSMakeRange([[buildOutput string] length], 0)]; // complete the lastIncompleteOutputLine from the first line of output if ([incommingLines count] > 0) { ASSIGN(lastIncompleteOutputLine, [lastIncompleteOutputLine stringByAppendingString: [incommingLines objectAtIndex: 0]]); [incommingLines removeObjectAtIndex: 0]; } // now if there are still more lines to process this means that // the lastIncompleteLine has been completed and start processing if ([incommingLines count] > 0) { NSEnumerator * e; NSString * line; NSMutableArray * linesToProcess = [NSMutableArray arrayWithCapacity: [incommingLines count]]; [linesToProcess addObject: lastIncompleteOutputLine]; // the new last incomplete output line will the last line of output ASSIGN(lastIncompleteOutputLine, [incommingLines lastObject]); [incommingLines removeLastObject]; [linesToProcess addObjectsFromArray: incommingLines]; // now process the completed lines e = [linesToProcess objectEnumerator]; while ((line = [e nextObject]) != nil) { if ([line hasPrefix: @"make["]) { unsigned int offset; // watch for directory changes if (ContainsString(line, _(@"Entering directory")) && (offset = [line rangeOfString: @"`"].location) != NSNotFound) { NSString * newDirectory = [line substringWithRange: NSMakeRange(offset, [line length] - offset - 1)]; [buildDirectoryStack addObject: newDirectory]; } else if (ContainsString(line, _(@"Leaving directory"))) { NSAssert([buildDirectoryStack count] > 1, @"Build " @"directory stack underflown."); [buildDirectoryStack removeLastObject]; } } } } [outputFileHandle waitForDataInBackgroundAndNotify]; } - (void) collectErrorOutput: (NSNotification *) notif { NSMutableArray * incommingLines; incommingLines = [[[[[[NSString alloc] initWithData: [errorFileHandle availableData] encoding: NSUTF8StringEncoding] autorelease] componentsSeparatedByString: @"\n"] mutableCopy] autorelease]; // complete the lastIncompleteErrorLine from the first line of error output if ([incommingLines count] > 0) { ASSIGN(lastIncompleteErrorLine, [lastIncompleteErrorLine stringByAppendingString: [incommingLines objectAtIndex: 0]]); [incommingLines removeObjectAtIndex: 0]; } // now if there are still more lines to process this means that // the lastIncompleteLine has been completed and start processing if ([incommingLines count] > 0) { NSEnumerator * e; NSString * line; NSMutableArray * linesToProcess = [NSMutableArray arrayWithCapacity: [incommingLines count]]; [linesToProcess addObject: lastIncompleteErrorLine]; // the new last incomplete error line will the last line of error output ASSIGN(lastIncompleteErrorLine, [incommingLines lastObject]); [incommingLines removeLastObject]; [linesToProcess addObjectsFromArray: incommingLines]; // now process the completed lines e = [linesToProcess objectEnumerator]; while ((line = [e nextObject]) != nil) { [buildErrorList addObject: ErrorInfoFromString(line, buildDirectoryStack)]; } // and finally tell the error table to update itself [buildErrors reloadData]; [buildErrors scrollPoint: NSMakePoint(0, [buildErrors frame].size.height)]; } [errorFileHandle waitForDataInBackgroundAndNotify]; } - (void) buildCompleted: (NSNotification *) notif { NSString * target = [buildTarget titleOfSelectedItem]; NSString * result; NSDictionary * userInfo; BOOL successFlag; NSNotificationCenter * nc = [NSNotificationCenter defaultCenter]; [nc removeObserver: self]; [self setBuilderState: MakeBuilderReady]; if ([task terminationStatus] == 0) { result = [NSString stringWithFormat: _(@"Build of target %@ SUCCEEDED"), target]; successFlag = YES; } else { result = [NSString stringWithFormat: _(@"Build of target %@ FAILED"), target]; successFlag = NO; } userInfo = [NSDictionary dictionaryWithObjectsAndKeys: document, @"Project", target, @"Target", [NSNumber numberWithBool: successFlag], @"Success", nil]; [nc postNotificationName: MakeBuilderBuildDidEndNotification object: self userInfo: userInfo]; [document logMessage: result]; DESTROY(task); DESTROY(outputFileHandle); DESTROY(errorFileHandle); } - (void) cleanCompleted: (NSNotification *) notif { NSString * target = [buildTarget titleOfSelectedItem]; NSString * result; NSDictionary * userInfo; BOOL successFlag; NSNotificationCenter * nc = [NSNotificationCenter defaultCenter]; [nc removeObserver: self]; [self setBuilderState: MakeBuilderReady]; if ([task terminationStatus] == 0) { result = [NSString stringWithFormat: _(@"Clean of target %@ SUCCEEDED"), target]; successFlag = YES; } else { result = [NSString stringWithFormat: _(@"Clean of target %@ FAILED"), target]; successFlag = NO; } userInfo = [NSDictionary dictionaryWithObjectsAndKeys: document, @"Project", target, @"Target", [NSNumber numberWithBool: successFlag], @"Success", nil]; [nc postNotificationName: MakeBuilderCleanDidEndNotification object: self userInfo: userInfo]; [document logMessage: result]; DESTROY(task); DESTROY(outputFileHandle); DESTROY(errorFileHandle); } /** * Action invoked when a build-option (like "Verbose Build" or "Warnings") * is changed. It simply marks the document as dirty. */ - (void) buildOptionChanged: sender { [document updateChangeCount: NSChangeDone]; } @end