Files
cappuccino/Tools/XcodeCapp/TNXCodeCapp.m
T

689 lines
25 KiB
Objective-C

/*
* This file is a part of program XcodeCapp
* Copyright (C) 2011 Antoine Mercadal (<primalmotion@archipelproject.org>)
*
* 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 3 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, see <http://www.gnu.org/licenses/>.
*/
#import "TNXCodeCapp.h"
#include "macros.h"
NSString * const XCCDidPopulateProjectNotification = @"XCCDidPopulateProjectNotification";
NSString * const XCCConversionStartNotification = @"XCCConversionStartNotification";
NSString * const XCCConversionStopNotification = @"XCCConversionStopNotification";
NSString * const XCCListeningStartNotification = @"XCCListeningStartNotification";
@implementation TNXCodeCapp
@synthesize delegate;
@synthesize errorList;
@synthesize XCodeSupportProject;
@synthesize currentProjectURL;
@synthesize currentProjectName;
@synthesize supportsFileBasedListening;
@synthesize reactToInodeModification;
@synthesize currentAPIMode;
@synthesize isListening;
@synthesize supportFileLevelAPI;
@synthesize isUsingFileLevelAPI;
#pragma mark -
#pragma mark Initialization
/*!
Initialize the AppController
*/
- (id)init
{
self = [super init];
if (self)
{
errorList = [NSMutableArray arrayWithCapacity:10];
fm = [NSFileManager defaultManager];
ignoredFilePaths = [NSMutableSet new];
parserPath = [[NSBundle mainBundle] pathForResource:@"parser" ofType:@"j"];
lastEventId = [[NSUserDefaults standardUserDefaults] objectForKey:@"lastEventId"];
appStartedTimestamp = [NSDate date];
[self setIsListening:NO];
[self setIsUsingFileLevelAPI:NO];
SInt32 versionMajor = 0;
SInt32 versionMinor = 0;
Gestalt(gestaltSystemVersionMajor, &versionMajor);
Gestalt(gestaltSystemVersionMinor, &versionMinor);
[self setSupportFileLevelAPI:versionMajor >= 10 && versionMinor >= 7];
// Uncomment to simulate 10.6 mode
//[self setSupportFileLevelAPI:NO];
[self configure];
if([fm fileExistsAtPath:[@"~/.bash_profile" stringByExpandingTildeInPath]])
profilePath = [@"source ~/.bash_profile" stringByExpandingTildeInPath];
else if([fm fileExistsAtPath:[@"~/.profile" stringByExpandingTildeInPath]])
profilePath = [@"source ~/.profile" stringByExpandingTildeInPath];
else if([fm fileExistsAtPath:[@"~/.bashrc" stringByExpandingTildeInPath]])
profilePath = [@"source ~/.bashrc" stringByExpandingTildeInPath];
else if([fm fileExistsAtPath:[@"~/.zshrc" stringByExpandingTildeInPath]])
profilePath = [@"source ~/.zshrc" stringByExpandingTildeInPath];
else
{
NSAlert *alert = [NSAlert alertWithMessageText:@"Cannot find any valid profile file."
defaultButton:@"OK"
alternateButton:nil
otherButton:nil
informativeTextWithFormat:@"Neither ~/.bash_profile, ~/.profile, ~/.bashrc nor ~/.zshrc can be found.\n\nWithout this XcodeCapp cannot locate nib2cib.\n\nIf you notice any errors or strange behaviour, please look at the system log for messages and open a ticket."];
[alert runModal];
profilePath = @"";
}
}
return self;
}
- (void)start
{
if (![[NSUserDefaults standardUserDefaults] boolForKey:@"XCCReopenLastProject"])
return;
NSString *lastOpenedPath = [[NSUserDefaults standardUserDefaults] objectForKey:@"LastOpenedPath"];
if (lastOpenedPath)
{
if ([fm fileExistsAtPath:lastOpenedPath])
{
[self listenProjectAtPath:[NSString stringWithFormat:@"%@/", lastOpenedPath]];
}
else
{
[[NSUserDefaults standardUserDefaults] removeObjectForKey:@"LastOpenedPath"];
}
}
}
/*!
Initializes the FSEvent stream
@param aPath the path of the folder to listen
*/
- (void)initializeEventStreamWithPath:(NSString*)aPath
{
NSArray *pathsToWatch = [NSArray arrayWithObject:aPath];
void *appPointer = (void *)self;
FSEventStreamContext context = {0, appPointer, NULL, NULL, NULL};
NSTimeInterval latency = 2.0;
FSEventStreamCreateFlags flags = 0;
if (stream != NULL)
free(stream);
if (supportsFileBasedListening)
{
DLog(@"Initializing the FSEventStream at file level (clean)");
flags = kFSEventStreamCreateFlagUseCFTypes | kFSEventStreamCreateFlagNoDefer | 0x00000010;
}
else
{
NSLog(@"Initializing the FSEventStream at folder level (dirty)");
flags = kFSEventStreamCreateFlagUseCFTypes;
}
stream = FSEventStreamCreate(NULL, &fsevents_callback, &context, (CFArrayRef) pathsToWatch,
[lastEventId unsignedLongLongValue], (CFAbsoluteTime) latency, flags);
FSEventStreamScheduleWithRunLoop(stream, CFRunLoopGetCurrent(), kCFRunLoopDefaultMode);
FSEventStreamStart(stream);
[self setIsListening:YES];
}
#pragma mark -
#pragma mark Utilities
/*!
Stop listening the FSEvent stream if active
*/
- (void)stopEventStream
{
if (stream)
{
FSEventStreamStop(stream);
FSEventStreamInvalidate(stream);
stream = nil;
}
[self setIsListening:NO];
}
/*!
Stops and clear the worker
*/
- (void)clear
{
currentProjectURL = nil;
currentProjectName = nil;
[ignoredFilePaths removeAllObjects];
[self stopEventStream];
}
/*!
Choose the API mode according to default
*/
- (void)configure
{
if (![self supportFileLevelAPI])
{
DLog(@"System doesn't support file level API");
[[NSUserDefaults standardUserDefaults] setObject:[NSNumber numberWithInt:2] forKey:@"XCCAPIMode"];
}
switch ([[NSUserDefaults standardUserDefaults] integerForKey:@"XCCAPIMode"])
{
case 0:
supportsFileBasedListening = [self supportFileLevelAPI] ? YES : NO;
break;
case 1:
supportsFileBasedListening = YES;
break;
case 2:
supportsFileBasedListening = NO;
break;
}
if (supportsFileBasedListening)
{
DLog(@"using 10.7+ mode listening (clean)");
[self setCurrentAPIMode:@"File level (Lion)"];
[self setIsUsingFileLevelAPI:YES];
reactToInodeModification = [[NSUserDefaults standardUserDefaults] boolForKey:@"XCCReactMode"];
}
else
{
DLog(@"using 10.6 mode listening (dirty)");
reactToInodeModification = NO;
[self setCurrentAPIMode:@"Folder level (Snow Leopard)"];
[self setIsUsingFileLevelAPI:NO];
}
}
/*!
Update the last event ID. We use a method because
This is called from outside the class, in the FSEvent callback
@param eventId the current event ID value
*/
- (void)updateLastEventId:(uint64_t)eventId
{
lastEventId = [NSNumber numberWithUnsignedLongLong:eventId];
[[NSUserDefaults standardUserDefaults] setObject:lastEventId forKey:@"lastEventId"];
[[NSUserDefaults standardUserDefaults] synchronize];
}
/*!
Run a NSTask with the given arguments
@param arguments NSArray containing the NSTask arguments
@return NSarray containing the return code (int) and the eventual response (string)
*/
- (NSArray *)runTask:(NSArray *)arguments
{
NSTask *task;
NSData *stdOut;
NSString *response;
NSNumber *status;
task = [[NSTask alloc] init];
[task setLaunchPath: @"/bin/bash"];
[task setArguments: arguments];
[task setStandardOutput:[NSPipe pipe]];
[task launch];
[task waitUntilExit];
stdOut = [[[task standardOutput] fileHandleForReading] availableData];
response = [[NSString alloc] initWithData:stdOut encoding:NSUTF8StringEncoding];
status = [NSNumber numberWithInt:[task terminationStatus]];
return [NSArray arrayWithObjects:status, response, nil];
}
/*!
Handle a file modification. If it's a .J or XIB or NIB, it will
perform the according conversion. If it's .xcodecapp-ignore, it will
update the list of ignored files.
@param fullPath the full path of the modified file
@param shouldNotify if YES, Growl notifications will be displayed
*/
- (void)handleFileModification:(NSString*)fullPath notify:(BOOL)shouldNotify
{
if ([self isPathMatchingIgnoredPaths:fullPath] || ![fm fileExistsAtPath:fullPath])
return;
DLog(@"Parsing modified file: %@", fullPath);
NSArray *arguments = nil;
NSString *successTitle = nil;
NSString *successMsg = nil;
NSString *response = nil;
NSNumber *status = [NSNumber numberWithInt:0];
NSString *splitPath = [fullPath substringFromIndex:[[currentProjectURL path] length] + 1];
NSString *shadowPath = [[self shadowURLForSourceURL:[NSURL URLWithString:[fullPath stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]]] path];
DLog(@"Shadow path: %@", shadowPath);
if ([self isXIBFile:fullPath] || [self isObjJFile:fullPath] || [self isXCCIgnoreFile:fullPath])
{
[[NSNotificationCenter defaultCenter] postNotificationName:XCCConversionStartNotification object:self];
if ([self isXIBFile:fullPath])
{
arguments = [NSArray arrayWithObjects: @"-c", [NSString stringWithFormat:@"(%@; nib2cib '%@';) 2>&1", profilePath, fullPath],@"",nil];
successTitle = @"XIB converted";
successMsg = splitPath;
}
else if ([self isObjJFile:fullPath])
{
arguments = [NSArray arrayWithObjects: @"-c", [NSString stringWithFormat:@"(%@; objj '%@' '%@' '%@';) 2>&1", profilePath, parserPath, fullPath, shadowPath],@"",nil];
successTitle = @"Objective-J source processed";
successMsg = splitPath;
}
else if ([self isXCCIgnoreFile:fullPath])
{
[self computeIgnoredPaths];
successTitle = @".xcodecapp-ignore processed";
successMsg = @"Ignored files list updated";
arguments = nil;
}
// Run the task and get the response if needed
if (arguments)
{
DLog(@"Running task...");
NSArray *statusInfo = [self runTask:arguments];
status = [statusInfo objectAtIndex:0];
response = [statusInfo objectAtIndex:1];
DLog(@"Task result/response: %@/%@", status, response);
}
if ([status intValue] == 0 && shouldNotify)
{
[delegate performSelector:@selector(growlWithTitle:message:) withObject:successTitle withObject:successMsg];
}
else if (![status intValue] == 0)
{
if (response)
[errorList addObject:response];
[delegate performSelector:@selector(growlWithTitle:message:) withObject:@"Error processing file" withObject:splitPath];
}
[[NSNotificationCenter defaultCenter] postNotificationName:XCCConversionStopNotification object:self];
DLog(@"Processed: %@", fullPath);
}
}
/*!
Handle a file deletion. If it's a .J, it will
remove the shadowed .h file. If it's .xcodecapp-ignore
it will reset the list of ignored files.
@param fullPath the full path of the modified file
@param shouldNotify if YES, Growl notifications will be displayed
*/
- (void)handleFileRemoval:(NSString*)fullPath
{
if ([self isPathMatchingIgnoredPaths:fullPath] || [fm fileExistsAtPath:fullPath])
return;
if ([self isObjJFile:fullPath])
{
NSString *shadowPath = [[self shadowURLForSourceURL:[NSURL URLWithString:[fullPath stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]]] path];
[fm removeItemAtPath:shadowPath error:nil];
DLog(@"Removing shadow file: %@", shadowPath);
}
else if ([self isXCCIgnoreFile:fullPath])
{
[self computeIgnoredPaths];
}
}
/*!
Check if given full path is Objective-J file
@param path the path to check
@return YES or NO
*/
- (BOOL)isObjJFile:(NSString *)path
{
return [[[path pathExtension] uppercaseString] isEqual:@"J"];
}
/*!
Check if given full path is XIB or NIB file
@param path the path to check
@return YES or NO
*/
- (BOOL)isXIBFile:(NSString *)path
{
path = [[path pathExtension] uppercaseString];
return [path isEqual:@"XIB"] || [path isEqual:@"NIB"];
}
/*!
Check if given full path is the .xcodecapp-ignore file
@param path the path to check
@return YES or NO
*/
- (BOOL)isXCCIgnoreFile:(NSString *)path
{
path = [path lastPathComponent];
return [path isEqual:@".xcodecapp-ignore"];
}
/*!
Check if .xCodeSupport needs to be initialized.
If not needed, check that all J files are mirrored. If no,
then launch conversion for missing mirrored h files
@return YES or NO
*/
- (BOOL)prepareXCodeSupportProject
{
XCodeSupportProjectName = [NSString stringWithFormat:@"%@.xcodeproj/", currentProjectName];
XCodeTemplatePBXPath = [[NSBundle mainBundle] pathForResource:@"project.pbxproj" ofType:@"sample"];
XCodeSupportFolder = [NSURL URLWithString:@".xCodeSupport/" relativeToURL:currentProjectURL];
XCodeSupportProject = [NSURL URLWithString:XCodeSupportProjectName relativeToURL:XCodeSupportFolder];
XCodeSupportProjectSources = [NSURL URLWithString:@"Sources/" relativeToURL:XCodeSupportFolder];
XCodeSupportPBXPath = [NSString stringWithFormat:@"%@/project.pbxproj", [XCodeSupportProject path]];
//[fm removeItemAtURL:XCodeSupportFolder error:nil];
// create the template project if it doesn't exist
if (![fm fileExistsAtPath:[XCodeSupportFolder path]])
{
NSLog(@"Xcode support folder created at: %@", [XCodeSupportProject path]);
[fm createDirectoryAtPath:[XCodeSupportProject path] withIntermediateDirectories:YES attributes:nil error:nil];
DLog(@"Copying project.pbxproj from %@ to %@", XCodeTemplatePBXPath, [XCodeSupportProject path]);
[fm copyItemAtPath:XCodeTemplatePBXPath toPath:XCodeSupportPBXPath error:nil];
DLog(@"Reading the content of the project.pbxproj");
NSMutableString *PBXContent = [NSMutableString stringWithContentsOfFile:XCodeSupportPBXPath encoding:NSUTF8StringEncoding error:nil];
[PBXContent replaceOccurrencesOfString:@"${CappuccinoProjectName}"
withString:currentProjectName
options:NSCaseInsensitiveSearch
range:NSMakeRange(0, [PBXContent length])];
[PBXContent replaceOccurrencesOfString:@"${CappuccinoProjectRelativePath}"
withString:@".."
options:NSCaseInsensitiveSearch
range:NSMakeRange(0, [PBXContent length])];
[PBXContent writeToFile:XCodeSupportPBXPath atomically:YES encoding:NSUTF8StringEncoding error:nil];
DLog(@"PBX file adapted to the project");
DLog(@"Creating source folder");
[fm createDirectoryAtPath:[XCodeSupportProjectSources path] withIntermediateDirectories:YES attributes:nil error:nil];
[NSThread detachNewThreadSelector:@selector(populateXCodeProject:)toTarget:self withObject:nil];
return NO;
}
return YES;
}
/*!
Initialize the creation of the .xCodeSupport project. This
Operation is threaded
@param arguments Thread arguments (not used)
*/
- (void)populateXCodeProject:(id)arguments
{
[delegate performSelector:@selector(growlWithTitle:message:) withObject:@"Loading project" withObject:[currentProjectURL path]];
//[self handleFileModification:[NSString stringWithFormat:@"%@", [currentProjectURL path]] notify:NO];
NSArray *subdpaths = [fm subpathsAtPath:[currentProjectURL path]];
for (NSString *p in subdpaths)
{
NSString *filePath = [NSString stringWithFormat:@"%@/%@", [currentProjectURL path], p];
DLog(@"Original import of %@", filePath);
[self handleFileModification:filePath notify:NO];
}
NSDictionary *info = [NSDictionary dictionaryWithObjectsAndKeys:currentProjectURL, @"URL", nil];
[[NSNotificationCenter defaultCenter] postNotificationName:XCCDidPopulateProjectNotification object:self userInfo:info];
}
/*!
Compute the mirorred (shadow) name for a given path
@param aSourceURL the origin path
@return NSURL representing the shadow path
*/
- (NSURL *)shadowURLForSourceURL:(NSURL*)aSourceURL
{
if (!aSourceURL)
[NSException raise:NSInvalidArgumentException format:@"shadowURLForSourceURL: aSource URL must not be null"];
NSMutableString *flattenedPath = [NSMutableString stringWithString:[aSourceURL path]];
NSMutableString *fileName = [NSMutableString stringWithString:[flattenedPath lastPathComponent]];
[fileName replaceOccurrencesOfString:@"_"
withString:@"-OLDUNDERSCORE-"
options:NSCaseInsensitiveSearch
range:NSMakeRange(0, [fileName length])];
[flattenedPath replaceOccurrencesOfString:[flattenedPath lastPathComponent]
withString:fileName
options:NSCaseInsensitiveSearch
range:NSMakeRange(0, [flattenedPath length])];
[flattenedPath replaceOccurrencesOfString:@"/"
withString:@"_"
options:NSCaseInsensitiveSearch
range:NSMakeRange(0, [flattenedPath length])];
DLog(@"Flattened path: %@", flattenedPath);
NSString *basename = [NSString stringWithFormat:@"%@.h", [[flattenedPath stringByDeletingPathExtension] stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]];
return [NSURL URLWithString:basename relativeToURL:XCodeSupportProjectSources];
}
- (NSURL *)sourceURLForShadowName:(NSString *)aString
{
NSMutableString * unshadowedPath = [NSMutableString stringWithString:aString];
[unshadowedPath replaceOccurrencesOfString:@"_"
withString:@"/"
options:NSCaseInsensitiveSearch
range:NSMakeRange(0, [unshadowedPath length])];
[unshadowedPath replaceOccurrencesOfString:@"-OLDUNDERSCORE-"
withString:@"_"
options:NSCaseInsensitiveSearch
range:NSMakeRange(0, [unshadowedPath length])];
[unshadowedPath replaceOccurrencesOfString:@".h"
withString:@".j"
options:NSCaseInsensitiveSearch
range:NSMakeRange(0, [unshadowedPath length])];
return [NSURL URLWithString:[NSString stringWithString:unshadowedPath]];
}
/*!
Compute the ignored paths according to any existing
.xcodecapp-ignore file
*/
- (void)computeIgnoredPaths
{
NSString *ignorePath = [NSString stringWithFormat:@"%@/.xcodecapp-ignore", [currentProjectURL path]];
[ignoredFilePaths removeAllObjects];
if ([fm fileExistsAtPath:ignorePath])
{
NSString *ignoreFileContent = [NSString stringWithContentsOfFile:ignorePath encoding:NSUTF8StringEncoding error:nil];
NSArray *ignoredPatterns = [ignoreFileContent componentsSeparatedByString:@"\n"];
for (NSString *pattern in ignoredPatterns)
{
if ([pattern length])
[ignoredFilePaths addObject:pattern];
}
}
[ignoredFilePaths addObject:@"*.git*"];
[ignoredFilePaths addObject:@"*.svn*"];
[ignoredFilePaths addObject:@"*.hg*"];
[ignoredFilePaths addObject:@"*Frameworks*"];
[ignoredFilePaths addObject:@"*.xCodeSupport*"];
[ignoredFilePaths addObject:@"*Build*"];
NSLog(@"Ignoring file paths: %@", ignoredFilePaths);
}
/*!
Check is given path should be ignored
@param aPath the path to check
@return YES if it should be ignored, NO otherwise
*/
- (BOOL)isPathMatchingIgnoredPaths:(NSString*)aPath
{
BOOL isMatching = NO;
for (NSString *ignoredPath in ignoredFilePaths)
{
if ([ignoredPath isEqual:@""])
continue;
NSMutableString *regexp = [NSMutableString stringWithFormat:@"%@/%@", [currentProjectURL path], ignoredPath];
[regexp replaceOccurrencesOfString:@"/"
withString:@"\\/"
options:NSCaseInsensitiveSearch
range:NSMakeRange(0, [regexp length])];
[regexp replaceOccurrencesOfString:@"."
withString:@"\\."
options:NSCaseInsensitiveSearch
range:NSMakeRange(0, [regexp length])];
[regexp replaceOccurrencesOfString:@"*"
withString:@".*"
options:NSCaseInsensitiveSearch
range:NSMakeRange(0, [regexp length])];
NSPredicate *regextest = [NSPredicate predicateWithFormat:@"SELF MATCHES %@", regexp];
if ([regextest evaluateWithObject:aPath])
{
isMatching = YES;
break;
}
}
return isMatching;
}
/*!
Start all needed processes for listening to a given path
@param path The folder path to listen to
*/
- (void)listenProjectAtPath:(NSString *)path
{
NSMutableString *tempName = [NSMutableString stringWithString:[path lastPathComponent]];
currentProjectURL = [NSURL URLWithString:path];
[tempName replaceOccurrencesOfString:@" "
withString:@"_"
options:NSCaseInsensitiveSearch
range:NSMakeRange(0, [tempName length])];
currentProjectName = [NSString stringWithString:tempName];
[self computeIgnoredPaths];
BOOL isProjectReady = [self prepareXCodeSupportProject];
[self initializeEventStreamWithPath:[currentProjectURL path]];
NSDictionary *info = [NSDictionary dictionaryWithObjectsAndKeys:path, @"path", [NSNumber numberWithInt:(isProjectReady) ? 1 : 0], @"ready", nil];
[[NSNotificationCenter defaultCenter] postNotificationName:XCCListeningStartNotification object:self userInfo:info];
[[NSUserDefaults standardUserDefaults] setObject:[currentProjectURL path] forKey:@"LastOpenedPath"];
}
/*!
Clean the support folder according to files present in given path
@param basePath the path of the folder to check
*/
- (void)tidyShadowedFiles:(NSString*)basePath
{
NSArray *subpaths = [[NSFileManager defaultManager] contentsOfDirectoryAtPath:[XCodeSupportProjectSources path] error:NULL];
for (NSString *subpath in subpaths)
{
NSString *unshadowed = [[self sourceURLForShadowName:subpath] path];
NSString *shadowFullPath = [NSString stringWithFormat:@"%@/%@", [XCodeSupportProjectSources path], subpath];
if (![fm fileExistsAtPath:unshadowed])
{
DLog(@"cleaning shadow file: %@", subpath);
[fm removeItemAtPath:shadowFullPath error:nil];
if (![self supportFileLevelAPI] && [self respondsToSelector:@selector(updateLastModificationDate:forPath:)])
[self performSelector:@selector(updateLastModificationDate:forPath:) withObject:nil withObject:unshadowed];
}
}
}
@end
@implementation TNXCodeCapp (SnowLeopard)
- (void)updateLastModificationDate:(NSDate *)date forPath:(NSString *)path
{
if (!pathModificationDates)
{
pathModificationDates = [[[NSUserDefaults standardUserDefaults] dictionaryForKey:@"pathModificationDates"] mutableCopy];
if (!pathModificationDates)
pathModificationDates = [NSMutableDictionary new];
}
if (date)
[pathModificationDates setObject:[date retain] forKey:path];
else
[pathModificationDates removeObjectForKey:path];
[[NSUserDefaults standardUserDefaults] setObject:pathModificationDates forKey:@"pathModificationDates"];
}
- (NSDate *)lastModificationDateForPath:(NSString *)path
{
if (!pathModificationDates)
{
pathModificationDates = [[[NSUserDefaults standardUserDefaults] dictionaryForKey:@"pathModificationDates"] mutableCopy];
if (!pathModificationDates)
pathModificationDates = [NSMutableDictionary new];
}
if ([pathModificationDates valueForKey:path] != nil)
return [pathModificationDates valueForKey:path];
else
return appStartedTimestamp;
}
@end