mirror of
https://github.com/cappuccino/cappuccino.git
synced 2026-08-27 05:57:04 +00:00
483 lines
19 KiB
Objective-C
483 lines
19 KiB
Objective-C
//
|
|
// CappuccinoProject.m
|
|
// XcodeCapp
|
|
//
|
|
// Created by Alexandre Wilhelm on 5/6/15.
|
|
// Copyright (c) 2015 cappuccino-project. All rights reserved.
|
|
//
|
|
|
|
#include <fcntl.h>
|
|
|
|
#import "XCCCappuccinoProject.h"
|
|
#import "XCCPath.h"
|
|
|
|
static NSArray * XCCDefaultBinaryPaths;
|
|
static NSDictionary* XCCCappuccinoProjectDefaultSettings;
|
|
static NSString * const XCCSlashReplacement = @"::";
|
|
|
|
static NSPredicate * XCCDirectoriesToIgnorePredicate = nil;
|
|
static NSPredicate * XCCXcodeCappNibToIgnorePredicate = nil;
|
|
static NSString * const XCCDirectoriesToIgnorePattern = @"^(?:Build|F(?:rameworks|oundation)|AppKit|Objective-J|(?:Browser|CommonJS)\\.environment|XcodeSupport|.+\\.xcodeproj)$";
|
|
static NSString * const XCCXcodeCappNibToIgnorePattern = @"^/Applications/XcodeCapp.app/.*$";
|
|
|
|
static NSArray *XCCCappuccinoProjectDefaultIgnoredPaths = nil;
|
|
|
|
NSString * const XCCCappuccinoProcessCappLintKey = @"XCCCappuccinoProcessCappLintKey";
|
|
NSString * const XCCCappuccinoProcessObjjKey = @"XCCCappuccinoProcessObjjKey";
|
|
NSString * const XCCCappuccinoProcessNib2CibKey = @"XCCCappuccinoProcessNib2CibKey";
|
|
NSString * const XCCCappuccinoProcessObjj2ObjcSkeletonKey = @"XCCCappuccinoProcessObjj2ObjcSkeletonKey";
|
|
NSString * const XCCCompatibilityVersionKey = @"XCCCompatibilityVersion";
|
|
NSString * const XCCCappuccinoProjectBinPathsKey = @"XCCCappuccinoProjectBinPathsKey";
|
|
NSString * const XCCCappuccinoObjjIncludePathKey = @"XCCCappuccinoObjjIncludePathKey";
|
|
NSString * const XCCCappuccinoProjectNicknameKey = @"XCCCappuccinoProjectNicknameKey";
|
|
NSString * const XCCCappuccinoProjectPreviousStatusKey = @"XCCCappuccinoProjectPreviousStatusKey";
|
|
NSString * const XCCCappuccinoProjectLastEventIDKey = @"XCCCappuccinoProjectLastEventIDKey";
|
|
|
|
|
|
|
|
|
|
@implementation XCCCappuccinoProject
|
|
|
|
@synthesize XcodeCappIgnoreContent = _XcodeCappIgnoreContent;
|
|
@synthesize status = _status;
|
|
|
|
#pragma mark - Class methods
|
|
|
|
+ (void)initialize
|
|
{
|
|
if (self != [XCCCappuccinoProject class])
|
|
return;
|
|
|
|
XCCDefaultBinaryPaths = @[[[XCCPath alloc] initWithName:@"~/bin"]];
|
|
|
|
NSNumber *appCompatibilityVersion = [[NSBundle mainBundle] objectForInfoDictionaryKey:XCCCompatibilityVersionKey];
|
|
|
|
XCCDirectoriesToIgnorePredicate = [NSPredicate predicateWithFormat:@"SELF matches %@", XCCDirectoriesToIgnorePattern];
|
|
XCCXcodeCappNibToIgnorePredicate = [NSPredicate predicateWithFormat:@"SELF matches %@", XCCXcodeCappNibToIgnorePattern];
|
|
|
|
XCCCappuccinoProjectDefaultSettings = @{XCCCompatibilityVersionKey: appCompatibilityVersion,
|
|
XCCCappuccinoProcessCappLintKey: @NO,
|
|
XCCCappuccinoProcessObjjKey: @NO,
|
|
XCCCappuccinoProcessNib2CibKey: @YES,
|
|
XCCCappuccinoProcessObjj2ObjcSkeletonKey: @YES,
|
|
XCCCappuccinoProjectBinPathsKey: [XCCDefaultBinaryPaths valueForKeyPath:@"name"],
|
|
XCCCappuccinoObjjIncludePathKey: @"",
|
|
XCCCappuccinoProjectNicknameKey: @"",
|
|
XCCCappuccinoProjectPreviousStatusKey: @0};
|
|
|
|
XCCCappuccinoProjectDefaultIgnoredPaths = @[
|
|
@"Frameworks/*",
|
|
@"Build/*",
|
|
@"*.xcodeproj/*",
|
|
@".XcodeSupport/*",
|
|
@"NS_*.j",
|
|
@".xcodecapp-ignore",
|
|
@"*.git/*",
|
|
@".cappenvs/*",
|
|
@"main.j",
|
|
@"!Frameworks/Sources"
|
|
];
|
|
}
|
|
|
|
+ (NSArray*)defaultBinaryPaths
|
|
{
|
|
return XCCDefaultBinaryPaths;
|
|
}
|
|
|
|
+ (BOOL)isObjjFile:(NSString *)path
|
|
{
|
|
return [path.pathExtension.lowercaseString isEqual:@"j"];
|
|
}
|
|
|
|
+ (BOOL)isCibFile:(NSString *)path
|
|
{
|
|
return [path.pathExtension.lowercaseString isEqual:@"cib"];
|
|
}
|
|
|
|
+ (BOOL)isHeaderFile:(NSString *)path
|
|
{
|
|
return [path.pathExtension.lowercaseString isEqual:@"h"];
|
|
}
|
|
|
|
+ (BOOL)isXibFile:(NSString *)path
|
|
{
|
|
NSString *extension = path.pathExtension.lowercaseString;
|
|
|
|
if ([extension isEqual:@"xib"] || [extension isEqual:@"nib"])
|
|
{
|
|
// Xcode creates temp files called <filename>~.xib. Filter those out.
|
|
NSString *baseFilename = path.lastPathComponent.stringByDeletingPathExtension;
|
|
|
|
return [baseFilename characterAtIndex:baseFilename.length - 1] != '~';
|
|
}
|
|
|
|
return NO;
|
|
}
|
|
|
|
+ (BOOL)isXCCIgnoreFile:(NSString *)path cappuccinoProject:(XCCCappuccinoProject*)aCappuccinoProject
|
|
{
|
|
return [path isEqualToString:aCappuccinoProject.XcodeCappIgnorePath];
|
|
}
|
|
|
|
+ (BOOL)isSourceFile:(NSString *)path cappuccinoProject:(XCCCappuccinoProject*)aCappuccinoProject
|
|
{
|
|
return ([self isXibFile:path] || [self isObjjFile:path]) && ![self pathMatchesIgnoredPaths:path cappuccinoProjectIgnoredPathPredicates:aCappuccinoProject.ignoredPathPredicates];
|
|
}
|
|
|
|
+ (BOOL)shouldIgnoreDirectoryNamed:(NSString *)filename
|
|
{
|
|
return [XCCDirectoriesToIgnorePredicate evaluateWithObject:filename];
|
|
}
|
|
|
|
+ (BOOL)pathMatchesIgnoredPaths:(NSString*)aPath cappuccinoProjectIgnoredPathPredicates:(NSMutableArray*)cappuccinoProjectIgnoredPathPredicates
|
|
{
|
|
BOOL ignore = NO;
|
|
|
|
// This is a bit tricky, is to avoid to fetch nib of a compiled XcodeCapp
|
|
if ([XCCXcodeCappNibToIgnorePredicate evaluateWithObject:aPath])
|
|
return YES;
|
|
|
|
for (NSDictionary *ignoreInfo in cappuccinoProjectIgnoredPathPredicates)
|
|
{
|
|
BOOL matches = [ignoreInfo[@"predicate"] evaluateWithObject:aPath];
|
|
|
|
if (matches)
|
|
ignore = [ignoreInfo[@"exclude"] boolValue];
|
|
}
|
|
|
|
return ignore;
|
|
}
|
|
|
|
+ (NSArray *)parseIgnorePaths:(NSArray *)paths basePath:(NSString *)basePath
|
|
{
|
|
NSMutableArray *parsedPaths = [@[] mutableCopy];
|
|
NSCharacterSet *whitespace = [NSCharacterSet whitespaceCharacterSet];
|
|
|
|
for (NSString *pattern in paths)
|
|
{
|
|
if ([pattern stringByTrimmingCharactersInSet:whitespace].length == 0)
|
|
continue;
|
|
|
|
BOOL exclude = [pattern characterAtIndex:0] != '!';
|
|
NSString *finalPattern = exclude ? pattern : [pattern substringFromIndex:1];
|
|
|
|
NSString *regexPattern = [self globToRegexPattern:[NSString stringWithFormat:@"%@/%@", basePath, finalPattern]];
|
|
NSPredicate *predicate = [NSPredicate predicateWithFormat:@"SELF matches %@", regexPattern];
|
|
[parsedPaths addObject:@{ @"predicate": predicate,
|
|
@"exclude": @(exclude)}];
|
|
}
|
|
|
|
return parsedPaths;
|
|
}
|
|
|
|
+ (NSString *)globToRegexPattern:(NSString *)glob
|
|
{
|
|
NSMutableString *regex = [glob mutableCopy];
|
|
|
|
if ([regex characterAtIndex:0] == '!')
|
|
[regex deleteCharactersInRange:NSMakeRange(0, 1)];
|
|
|
|
[regex replaceOccurrencesOfString:@"."
|
|
withString:@"\\."
|
|
options:0
|
|
range:NSMakeRange(0, [regex length])];
|
|
|
|
[regex replaceOccurrencesOfString:@"*"
|
|
withString:@".*"
|
|
options:0
|
|
range:NSMakeRange(0, [regex length])];
|
|
|
|
// If the glob ends with "/", match that directory and anything below it.
|
|
if ([regex characterAtIndex:regex.length - 1] == '/')
|
|
[regex replaceCharactersInRange:NSMakeRange(regex.length - 1, 1) withString:@"(?:/.*)?"];
|
|
|
|
return [NSString stringWithFormat:@"^%@$", regex];
|
|
}
|
|
|
|
+ (NSArray *)getPathsToWatchForCappuccinoProject:(XCCCappuccinoProject*)aCappuccinoProject
|
|
{
|
|
NSMutableArray *pathsToWatch = [@[aCappuccinoProject.projectPath] mutableCopy];
|
|
NSArray *otherPathsToWatch = @[@"", @"Frameworks/Debug", @"Frameworks/Source"];
|
|
NSFileManager *fm = [NSFileManager defaultManager];
|
|
|
|
for (NSString *path in otherPathsToWatch)
|
|
{
|
|
NSString *fullPath = [aCappuccinoProject.projectPath stringByAppendingPathComponent:path];
|
|
|
|
BOOL exists, isDirectory;
|
|
exists = [fm fileExistsAtPath:fullPath isDirectory:&isDirectory];
|
|
|
|
if (exists && isDirectory)
|
|
[self watchSymlinkedDirectoriesAtPath:path pathsToWatch:pathsToWatch cappuccinoProject:aCappuccinoProject];
|
|
}
|
|
|
|
return [pathsToWatch copy];
|
|
}
|
|
|
|
+ (void)watchSymlinkedDirectoriesAtPath:(NSString *)projectPath pathsToWatch:(NSMutableArray *)pathsToWatch cappuccinoProject:(XCCCappuccinoProject*)aCappuccinoProject
|
|
{
|
|
NSString *fullProjectPath = [aCappuccinoProject.projectPath stringByAppendingPathComponent:projectPath];
|
|
NSError *error = NULL;
|
|
NSFileManager *fm = [NSFileManager defaultManager];
|
|
|
|
NSArray *urls = [fm contentsOfDirectoryAtURL:[NSURL fileURLWithPath:fullProjectPath]
|
|
includingPropertiesForKeys:@[NSURLIsDirectoryKey, NSURLIsSymbolicLinkKey]
|
|
options:NSDirectoryEnumerationSkipsHiddenFiles | NSDirectoryEnumerationSkipsPackageDescendants | NSDirectoryEnumerationSkipsSubdirectoryDescendants
|
|
error:&error];
|
|
|
|
for (NSURL *url in urls)
|
|
{
|
|
NSNumber *isSymlink;
|
|
[url getResourceValue:&isSymlink forKey:NSURLIsSymbolicLinkKey error:nil];
|
|
|
|
if (isSymlink.boolValue == NO)
|
|
continue;
|
|
|
|
NSURL *resolvedURL = [url URLByResolvingSymlinksInPath];
|
|
|
|
if (![resolvedURL checkResourceIsReachableAndReturnError:nil])
|
|
continue;
|
|
|
|
NSNumber *isDirectory;
|
|
[resolvedURL getResourceValue:&isDirectory forKey:NSURLIsDirectoryKey error:nil];
|
|
|
|
if (isDirectory.boolValue == NO)
|
|
continue;
|
|
|
|
NSString *path = resolvedURL.path;
|
|
NSString *filename = path.lastPathComponent;
|
|
|
|
if (![self shouldIgnoreDirectoryNamed:filename] && ![self pathMatchesIgnoredPaths:path cappuccinoProjectIgnoredPathPredicates:aCappuccinoProject.ignoredPathPredicates])
|
|
{
|
|
NSLog(@"Watching symlinked directory: %@", path);
|
|
|
|
[pathsToWatch addObject:path];
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
#pragma mark - Init methods
|
|
|
|
- (instancetype)initWithPath:(NSString*)aPath
|
|
{
|
|
if (self = [super init])
|
|
{
|
|
self.name = [aPath lastPathComponent];
|
|
self.nickname = self.name;
|
|
self.projectPath = aPath;
|
|
self.PBXModifierScriptPath = [[NSBundle mainBundle].resourcePath stringByAppendingPathComponent:@"pbxprojModifier.py"];
|
|
self.XcodeProjectPath = [self.projectPath stringByAppendingPathComponent:[[self.projectPath lastPathComponent] stringByAppendingString:@".xcodeproj"]];
|
|
self.XcodeCappIgnorePath = [self.projectPath stringByAppendingPathComponent:@".xcodecapp-ignore"];
|
|
self.supportPath = [self.projectPath stringByAppendingPathComponent:@".XcodeSupport"];
|
|
self.settingsPath = [self.supportPath stringByAppendingPathComponent:@"Info.plist"];
|
|
|
|
[self _loadSettings];
|
|
|
|
[self reloadXcodeCappIgnoreFile];
|
|
|
|
[self reinitialize];
|
|
}
|
|
|
|
return self;
|
|
}
|
|
|
|
- (void)reinitialize
|
|
{
|
|
self.projectPathsForSourcePaths = [@{} mutableCopy];
|
|
self.status = XCCCappuccinoProjectStatusStopped;
|
|
}
|
|
|
|
|
|
#pragma mark - Settings Management
|
|
|
|
- (id)_defaultSettings
|
|
{
|
|
NSMutableDictionary *defaultSettings = [XCCCappuccinoProjectDefaultSettings mutableCopy];
|
|
|
|
defaultSettings[XCCCappuccinoObjjIncludePathKey] = [NSString stringWithFormat:@"%@/%@", self.projectPath, @"Frameworks/Debug"];
|
|
defaultSettings[XCCCappuccinoProjectNicknameKey] = self.nickname;
|
|
|
|
return defaultSettings;
|
|
}
|
|
|
|
- (void)_loadSettings
|
|
{
|
|
self->settings = [[NSDictionary dictionaryWithContentsOfFile:self.settingsPath] mutableCopy];
|
|
|
|
if (!self->settings)
|
|
self->settings = [[self _defaultSettings] mutableCopy];
|
|
|
|
NSMutableArray *mutablePaths = [@[] mutableCopy];
|
|
NSArray *paths = self->settings[XCCCappuccinoProjectBinPathsKey];
|
|
|
|
if (paths)
|
|
{
|
|
for (NSString *name in paths)
|
|
{
|
|
XCCPath *path = [XCCPath new];
|
|
[path setName:name];
|
|
[mutablePaths addObject:path];
|
|
}
|
|
}
|
|
else
|
|
{
|
|
mutablePaths = [[[self class] defaultBinaryPaths] mutableCopy];
|
|
}
|
|
|
|
self.binaryPaths = mutablePaths;
|
|
self.nickname = self->settings[XCCCappuccinoProjectNicknameKey];
|
|
self.objjIncludePath = self->settings[XCCCappuccinoObjjIncludePathKey];
|
|
self.version = self->settings[XCCCompatibilityVersionKey];
|
|
self.processObjjWarnings = [self->settings[XCCCappuccinoProcessObjjKey] boolValue];
|
|
self.processCappLint = [self->settings[XCCCappuccinoProcessCappLintKey] boolValue];
|
|
self.processObjj2ObjcSkeleton = [self->settings[XCCCappuccinoProcessObjj2ObjcSkeletonKey] boolValue];
|
|
self.processNib2Cib = [self->settings[XCCCappuccinoProcessNib2CibKey] boolValue];
|
|
self.previousSavedStatus = [self->settings[XCCCappuccinoProjectPreviousStatusKey] intValue];
|
|
|
|
if (self->settings[XCCCappuccinoProjectLastEventIDKey])
|
|
self.lastEventID = self->settings[XCCCappuccinoProjectLastEventIDKey];
|
|
}
|
|
|
|
- (void)_writeSettings
|
|
{
|
|
NSData *data = [NSPropertyListSerialization dataFromPropertyList:self->settings format:NSPropertyListXMLFormat_v1_0 errorDescription:nil];
|
|
|
|
[data writeToFile:self.settingsPath atomically:YES];
|
|
}
|
|
|
|
- (void)saveSettings
|
|
{
|
|
self->settings[XCCCappuccinoProjectBinPathsKey] = [self.binaryPaths valueForKeyPath:@"name"];
|
|
self->settings[XCCCappuccinoObjjIncludePathKey] = self.objjIncludePath;
|
|
self->settings[XCCCappuccinoProjectNicknameKey] = self.nickname;
|
|
self->settings[XCCCompatibilityVersionKey] = self.version;
|
|
self->settings[XCCCappuccinoProcessObjjKey] = @(self.processObjjWarnings);
|
|
self->settings[XCCCappuccinoProcessCappLintKey] = @(self.processCappLint);
|
|
self->settings[XCCCappuccinoProcessObjj2ObjcSkeletonKey] = @(self.processObjj2ObjcSkeleton);
|
|
self->settings[XCCCappuccinoProcessNib2CibKey] = @(self.processNib2Cib);
|
|
self->settings[XCCCappuccinoProjectPreviousStatusKey] = [NSNumber numberWithInt:self.status];
|
|
|
|
if ([self.lastEventID boolValue])
|
|
self->settings[XCCCappuccinoProjectLastEventIDKey] = self.lastEventID;
|
|
|
|
[self _writeXcodeCappIgnoreFile];
|
|
[self _writeSettings];
|
|
}
|
|
|
|
|
|
#pragma mark - XcodeCapp Ignore
|
|
|
|
- (void)_writeXcodeCappIgnoreFile
|
|
{
|
|
NSData *data = [NSPropertyListSerialization dataFromPropertyList:self->settings format:NSPropertyListXMLFormat_v1_0 errorDescription:nil];
|
|
|
|
[data writeToFile:self.settingsPath atomically:YES];
|
|
|
|
NSFileManager *fm = [NSFileManager defaultManager];
|
|
|
|
if ([self.XcodeCappIgnoreContent length])
|
|
[self.XcodeCappIgnoreContent writeToFile:self.XcodeCappIgnorePath atomically:YES encoding:NSASCIIStringEncoding error:nil];
|
|
else if ([fm fileExistsAtPath:self.XcodeCappIgnorePath])
|
|
[fm removeItemAtPath:self.XcodeCappIgnorePath error:nil];
|
|
}
|
|
|
|
- (void)_updateXcodeCappIgnorePredicates
|
|
{
|
|
self.ignoredPathPredicates = [@[] mutableCopy];
|
|
|
|
@try
|
|
{
|
|
NSMutableArray *ignoredPatterns = [@[] mutableCopy];
|
|
|
|
for (NSString *pattern in XCCCappuccinoProjectDefaultIgnoredPaths)
|
|
[ignoredPatterns addObject:pattern];
|
|
|
|
for (NSString *pattern in [self.XcodeCappIgnoreContent componentsSeparatedByString:@"\n"])
|
|
if ([pattern length])
|
|
[ignoredPatterns addObject:pattern];
|
|
|
|
NSArray *parsedPaths = [XCCCappuccinoProject parseIgnorePaths:ignoredPatterns basePath:self.projectPath];
|
|
[self.ignoredPathPredicates addObjectsFromArray:parsedPaths];
|
|
|
|
NSLog(@"Content of xcodecapp-ignorepath correctly updated %@", self.ignoredPathPredicates);
|
|
}
|
|
@catch(NSException *exception)
|
|
{
|
|
NSLog(@"Content of xcodecapp-ignorepath does not math the expected input");
|
|
self.ignoredPathPredicates = [@[] mutableCopy];
|
|
}
|
|
}
|
|
|
|
- (void)reloadXcodeCappIgnoreFile
|
|
{
|
|
NSFileManager *fm = [NSFileManager defaultManager];
|
|
|
|
if ([fm fileExistsAtPath:self.XcodeCappIgnorePath])
|
|
self.XcodeCappIgnoreContent = [NSString stringWithContentsOfFile:self.XcodeCappIgnorePath encoding:NSASCIIStringEncoding error:nil];
|
|
else
|
|
self.XcodeCappIgnoreContent = @""; //otherwise we prepare the xcc ignore with the default values
|
|
}
|
|
|
|
|
|
#pragma mark Paths Management
|
|
|
|
- (NSString *)projectRelativePathForPath:(NSString *)path
|
|
{
|
|
return [path substringFromIndex:self.projectPath.length + 1];
|
|
}
|
|
|
|
- (NSString *)projectPathForSourcePath:(NSString *)path
|
|
{
|
|
NSString *base = path.stringByDeletingLastPathComponent;
|
|
NSString *projectPath = self.projectPathsForSourcePaths[base];
|
|
|
|
return projectPath ? [projectPath stringByAppendingPathComponent:path.lastPathComponent] : path;
|
|
}
|
|
|
|
- (NSString *)shadowBasePathForProjectSourcePath:(NSString *)path
|
|
{
|
|
if (path.isAbsolutePath)
|
|
path = [self projectRelativePathForPath:path];
|
|
|
|
NSString *filename = [path.stringByDeletingPathExtension stringByReplacingOccurrencesOfString:@"/" withString:XCCSlashReplacement];
|
|
|
|
return [self.supportPath stringByAppendingPathComponent:filename];
|
|
}
|
|
|
|
- (NSString *)sourcePathForShadowPath:(NSString *)path
|
|
{
|
|
NSString *filename = [path stringByReplacingOccurrencesOfString:XCCSlashReplacement withString:@"/"];
|
|
filename = [filename.stringByDeletingPathExtension stringByAppendingPathExtension:@"j"];
|
|
|
|
return [self.projectPath stringByAppendingPathComponent:filename];
|
|
}
|
|
|
|
- (NSString *)flattenedXcodeSupportFileNameForPath:(NSString *)aPath
|
|
{
|
|
NSString *relativePath = [[aPath stringByDeletingPathExtension] stringByReplacingOccurrencesOfString:[self.projectPath stringByAppendingString:@"/"] withString:@""];
|
|
|
|
return [relativePath stringByReplacingOccurrencesOfString:@"/" withString:XCCSlashReplacement];
|
|
}
|
|
|
|
|
|
#pragma mark - Custom Getters and Setters
|
|
|
|
- (NSString *)XcodeCappIgnoreContent
|
|
{
|
|
return _XcodeCappIgnoreContent;
|
|
}
|
|
|
|
- (void)setXcodeCappIgnoreContent:(NSString *)XcodeCappIgnoreContent
|
|
{
|
|
if ([XcodeCappIgnoreContent isEqualToString:_XcodeCappIgnoreContent])
|
|
return;
|
|
|
|
[self willChangeValueForKey:@"XcodeCappIgnoreContent"];
|
|
_XcodeCappIgnoreContent = XcodeCappIgnoreContent;
|
|
[self _updateXcodeCappIgnorePredicates];
|
|
[self didChangeValueForKey:@"XcodeCappIgnoreContent"];
|
|
}
|
|
|
|
@end
|