Skip to content

Commit c1b89dc

Browse files
author
Mihail Slavchev
committed
Merge pull request #230 from fealebenpae/library-add
Library add
2 parents b8945d8 + 35f116e commit c1b89dc

File tree

16 files changed

+307
-9
lines changed

16 files changed

+307
-9
lines changed

lib/bootstrap.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ $injector.requireCommand("platform|*list", "./commands/list-platforms");
2626
$injector.requireCommand("platform|add", "./commands/add-platform");
2727
$injector.requireCommand("platform|remove", "./commands/remove-platform");
2828
$injector.requireCommand("platform|update", "./commands/update-platform");
29+
$injector.requireCommand("library|add", "./commands/add-library");
2930
$injector.requireCommand("run|ios", "./commands/run");
3031
$injector.requireCommand("run|android", "./commands/run");
3132
$injector.requireCommand("debug", "./commands/debug");

lib/commands/add-library.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
///<reference path="../.d.ts"/>
2+
"use strict";
3+
4+
import path = require("path");
5+
import Future = require("fibers/future");
6+
7+
export class AddLibraryCommand implements ICommand {
8+
constructor(private $platformService: IPlatformService,
9+
private $errors: IErrors) { }
10+
11+
allowedParameters: ICommandParameter[] = [];
12+
13+
execute(args: string[]): IFuture<void> {
14+
var platform = args[0];
15+
var libraryPath = path.resolve(args[1]);
16+
return this.$platformService.addLibrary(platform, libraryPath);
17+
}
18+
19+
canExecute(args: string[]): IFuture<boolean> {
20+
if (args.length !== 2) {
21+
this.$errors.fail("This command needs two parameters.");
22+
}
23+
24+
this.$platformService.validatePlatformInstalled(args[0]);
25+
return Future.fromResult(true);
26+
}
27+
}
28+
$injector.registerCommand("library|add", AddLibraryCommand);

lib/definitions/libref.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
interface ILibRef {
2+
idx: number;
3+
path: string;
4+
adjustedPath?: string;
5+
}

lib/definitions/osenv.d.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
declare module "osenv" {
22
function home(): string;
3-
}
3+
}

lib/definitions/platform.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ interface IPlatformService {
1313
deployOnEmulator(platform: string): IFuture<void>;
1414
validatePlatformInstalled(platform: string): void;
1515
validatePlatform(platform: string): void;
16+
addLibrary(platform: string, libraryPath: string): IFuture<void>;
1617
}
1718

1819
interface IPlatformData {

lib/definitions/project.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,4 +29,5 @@ interface IPlatformProjectService {
2929
prepareProject(platformData: IPlatformData): IFuture<string>;
3030
buildProject(projectRoot: string): IFuture<void>;
3131
isPlatformPrepared(projectRoot: string): IFuture<boolean>;
32+
addLibrary(platformData: IPlatformData, libraryPath: string): IFuture<void>;
3233
}

lib/definitions/xcode.d.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
declare module "xcode" {
2+
interface FrameworkOptions {
3+
[key: string]: any;
4+
5+
customFramework?: boolean;
6+
7+
embed?: boolean;
8+
}
9+
10+
class project {
11+
constructor(filename: string);
12+
13+
parse(callback: () => void): void;
14+
parseSync(): void;
15+
16+
writeSync(): string;
17+
18+
addFramework(filepath: string, options?: FrameworkOptions): void;
19+
removeFramework(filePath: string, options?: FrameworkOptions): void;
20+
21+
updateBuildProperty(key: string, value: any): void;
22+
}
23+
}

lib/services/android-project-service.ts

Lines changed: 123 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,12 @@ import options = require("../common/options");
88
import constants = require("../constants");
99
import hostInfo = require("../common/host-info");
1010
import helpers = require("../common/helpers");
11+
import fs = require("fs");
12+
import os = require("os");
1113

1214
class AndroidProjectService implements IPlatformProjectService {
1315
private SUPPORTED_TARGETS = ["android-17", "android-18", "android-19", "android-21"];
16+
private static METADATA_DIRNAME = "__metadata";
1417
private targetApi: string;
1518

1619
constructor(private $androidEmulatorServices: Mobile.IEmulatorPlatformServices,
@@ -118,19 +121,134 @@ class AndroidProjectService implements IPlatformProjectService {
118121
return assetsDirectory;
119122

120123
}).future<string>()();
121-
}
124+
}
125+
126+
private updateMetadata(projectRoot: string): void {
127+
var projMetadataDir = path.join(projectRoot, "assets", "metadata");
128+
var libsmetadataDir = path.join(projectRoot, "../../lib", this.platformData.normalizedPlatformName, AndroidProjectService.METADATA_DIRNAME);
129+
shell.cp("-f", path.join(libsmetadataDir, "*.dat"), projMetadataDir);
130+
}
131+
132+
private generateMetadata(projectRoot: string): void {
133+
var metadataGeneratorPath = path.join(__dirname, "../../resources/tools/metadata-generator.jar");
134+
var libsFolder = path.join(projectRoot, "../../lib", this.platformData.normalizedPlatformName);
135+
var metadataDirName = AndroidProjectService.METADATA_DIRNAME;
136+
var outDir = path.join(libsFolder, metadataDirName);
137+
this.$fs.ensureDirectoryExists(outDir).wait();
138+
139+
shell.cp("-f", path.join(__dirname, "../../resources/tools/android.jar"), libsFolder);
140+
shell.cp("-f", path.join(__dirname, "../../resources/tools/android-support-v4.jar"), libsFolder);
141+
shell.cp("-f", path.join(projectRoot, "libs/*.jar"), libsFolder);
142+
143+
this.spawn('java', ['-jar', metadataGeneratorPath, libsFolder, outDir]).wait();
144+
}
122145

123146
public buildProject(projectRoot: string): IFuture<void> {
124147
return (() => {
125148
var buildConfiguration = options.release ? "release" : "debug";
126-
var args = this.getAntArgs(buildConfiguration, projectRoot);
127-
this.spawn('ant', args).wait();
149+
var args = this.getAntArgs(buildConfiguration, projectRoot);
150+
var argsSaved = this.getAntArgs(buildConfiguration, projectRoot);
151+
this.spawn('ant', args).wait();
152+
this.generateMetadata(projectRoot);
153+
this.updateMetadata(projectRoot);
154+
// build the project again in order to include the newly generated metadata
155+
this.spawn('ant', argsSaved).wait();
128156
}).future<void>()();
129157
}
130158

131159
public isPlatformPrepared(projectRoot: string): IFuture<boolean> {
132160
return this.$fs.exists(path.join(projectRoot, "assets", constants.APP_FOLDER_NAME));
133-
}
161+
}
162+
163+
private generateBuildFile(projDir: string, targetSdk: string): void {
164+
this.$logger.info("Generate build.xml for %s", projDir);
165+
var cmd = util.format("android update project -p %s --target %s --subprojects", projDir, targetSdk);
166+
this.$childProcess.exec(cmd).wait();
167+
}
168+
169+
private parseProjectProperties(projDir: string, destDir: string): void {
170+
var projProp = path.join(projDir, "project.properties");
171+
172+
if (!this.$fs.exists(projProp).wait()) {
173+
this.$logger.warn("File %s does not exist", projProp);
174+
return;
175+
}
176+
177+
var lines = this.$fs.readText(projProp, "utf-8").wait().split(os.EOL);
178+
179+
var regEx = /android\.library\.reference\.(\d+)=(.*)/;
180+
lines.forEach(elem => {
181+
var match = elem.match(regEx);
182+
if (match) {
183+
var libRef: ILibRef = { idx: parseInt(match[1]), path: match[2].trim() };
184+
libRef.adjustedPath = this.$fs.isRelativePath(libRef.path) ? path.join(projDir, libRef.path) : libRef.path;
185+
this.parseProjectProperties(libRef.adjustedPath, destDir);
186+
}
187+
});
188+
189+
this.$logger.info("Copying %s", projDir);
190+
shell.cp("-Rf", projDir, destDir);
191+
192+
var targetDir = path.join(destDir, path.basename(projDir));
193+
// TODO: parametrize targetSdk
194+
var targetSdk = "android-17";
195+
this.generateBuildFile(targetDir, targetSdk);
196+
}
197+
198+
private getProjectReferences(projDir: string): ILibRef[]{
199+
var projProp = path.join(projDir, "project.properties");
200+
201+
var lines = this.$fs.readText(projProp, "utf-8").wait().split(os.EOL);
202+
203+
var refs: ILibRef[] = [];
204+
205+
var regEx = /android\.library\.reference\.(\d+)=(.*)/;
206+
lines.forEach(elem => {
207+
var match = elem.match(regEx);
208+
if (match) {
209+
var libRef: ILibRef = { idx: parseInt(match[1]), path: match[2] };
210+
libRef.adjustedPath = path.join(projDir, libRef.path);
211+
refs.push(libRef);
212+
}
213+
});
214+
215+
return refs;
216+
}
217+
218+
private updateProjectReferences(projDir: string, libraryPath: string): void {
219+
var refs = this.getProjectReferences(projDir);
220+
var maxIdx = refs.length > 0 ? _.max(refs, r => r.idx).idx : 0;
221+
222+
var relLibDir = path.relative(projDir, libraryPath).split("\\").join("/");
223+
224+
var libRefExists = _.any(refs, r => path.normalize(r.path) === path.normalize(relLibDir));
225+
226+
if (!libRefExists) {
227+
var projRef = util.format("%sandroid.library.reference.%d=%s", os.EOL, maxIdx + 1, relLibDir);
228+
var projProp = path.join(projDir, "project.properties");
229+
fs.appendFileSync(projProp, projRef, { encoding: "utf-8" });
230+
}
231+
}
232+
233+
public addLibrary(platformData: IPlatformData, libraryPath: string): IFuture<void> {
234+
return (() => {
235+
var name = path.basename(libraryPath);
236+
var projDir = this.$projectData.projectDir;
237+
var targetPath = path.join(projDir, "lib", platformData.normalizedPlatformName);
238+
this.$fs.ensureDirectoryExists(targetPath).wait();
239+
240+
this.parseProjectProperties(libraryPath, targetPath);
241+
242+
shell.cp("-f", path.join(libraryPath, "*.jar"), targetPath);
243+
var projectLibsDir = path.join(platformData.projectRoot, "libs");
244+
this.$fs.ensureDirectoryExists(projectLibsDir).wait();
245+
shell.cp("-f", path.join(libraryPath, "*.jar"), projectLibsDir);
246+
247+
var targetLibPath = path.join(targetPath, path.basename(libraryPath));
248+
249+
this.updateProjectReferences(platformData.projectRoot, targetLibPath);
250+
}).future<void>()();
251+
}
134252

135253
public getFrameworkFilesExtensions(): string[] {
136254
return [".jar", ".dat"];
@@ -325,4 +443,4 @@ class AndroidProjectService implements IPlatformProjectService {
325443
}).future<void>()();
326444
}
327445
}
328-
$injector.register("androidProjectService", AndroidProjectService);
446+
$injector.register("androidProjectService", AndroidProjectService);

lib/services/ios-project-service.ts

Lines changed: 91 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,19 +5,21 @@ import Future = require("fibers/future");
55
import path = require("path");
66
import shell = require("shelljs");
77
import util = require("util");
8+
import xcode = require("xcode");
89
import constants = require("./../constants");
910
import helpers = require("./../common/helpers");
1011
import options = require("../common/options");
1112

1213
class IOSProjectService implements IPlatformProjectService {
1314
private static XCODE_PROJECT_EXT_NAME = ".xcodeproj";
14-
private static XCODEBUILD_MIN_VERSION = "5.0";
15+
private static XCODEBUILD_MIN_VERSION = "6.0";
1516
private static IOS_PROJECT_NAME_PLACEHOLDER = "__PROJECT_NAME__";
1617

1718
constructor(private $projectData: IProjectData,
1819
private $fs: IFileSystem,
1920
private $childProcess: IChildProcess,
2021
private $errors: IErrors,
22+
private $logger: ILogger,
2123
private $iOSEmulatorServices: Mobile.IEmulatorPlatformServices) { }
2224

2325
public get platformData(): IPlatformData {
@@ -169,6 +171,93 @@ class IOSProjectService implements IPlatformProjectService {
169171
return this.$fs.exists(path.join(projectRoot, this.$projectData.projectName, constants.APP_FOLDER_NAME));
170172
}
171173

174+
public addLibrary(platformData: IPlatformData, libraryPath: string): IFuture<void> {
175+
return (() => {
176+
this.validateDynamicFramework(libraryPath).wait();
177+
var umbrellaHeader = this.getUmbrellaHeaderFromDynamicFramework(libraryPath).wait();
178+
179+
var frameworkName = path.basename(libraryPath, path.extname(libraryPath));
180+
var targetPath = path.join(this.$projectData.projectDir, "lib", platformData.normalizedPlatformName, frameworkName);
181+
this.$fs.ensureDirectoryExists(targetPath).wait();
182+
shell.cp("-R", libraryPath, targetPath);
183+
184+
this.generateFrameworkMetadata(platformData.projectRoot, targetPath, frameworkName, umbrellaHeader).wait();
185+
186+
var pbxProjPath = path.join(platformData.projectRoot, this.$projectData.projectName + ".xcodeproj", "project.pbxproj");
187+
var project = new xcode.project(pbxProjPath);
188+
project.parseSync();
189+
190+
project.addFramework(path.join(targetPath, frameworkName + ".framework"), { customFramework: true, embed: true });
191+
project.updateBuildProperty("IPHONEOS_DEPLOYMENT_TARGET", "8.0");
192+
this.$fs.writeFile(pbxProjPath, project.writeSync()).wait();
193+
this.$logger.info("The iOS Deployment Target is now 8.0 in order to support Cocoa Touch Frameworks.");
194+
}).future<void>()();
195+
}
196+
197+
private validateDynamicFramework(libraryPath: string): IFuture<void> {
198+
return (() => {
199+
var infoPlistPath = path.join(libraryPath, "Info.plist");
200+
if (!this.$fs.exists(infoPlistPath).wait()) {
201+
this.$errors.failWithoutHelp("The bundle at %s does not contain an Info.plist file.", libraryPath);
202+
}
203+
204+
var packageType = this.$childProcess.exec('/usr/libexec/PlistBuddy -c "Print :CFBundlePackageType" ' + infoPlistPath).wait().trim();
205+
if (packageType !== "FMWK") {
206+
this.$errors.failWithoutHelp("The bundle at %s does not appear to be a dynamic framework.", libraryPath);
207+
}
208+
}).future<void>()();
209+
}
210+
211+
private getUmbrellaHeaderFromDynamicFramework(libraryPath: string): IFuture<string> {
212+
return (() => {
213+
var modulemapPath = path.join(libraryPath, "Modules", "module.modulemap");
214+
if (!this.$fs.exists(modulemapPath).wait()) {
215+
this.$errors.failWithoutHelp("The framework at %s does not contain a module.modulemap file.", modulemapPath);
216+
}
217+
218+
var modulemap = this.$fs.readText(modulemapPath).wait();
219+
var umbrellaRegex = /umbrella header "(.+\.h)"/g;
220+
var match = umbrellaRegex.exec(modulemap);
221+
if (!match) {
222+
this.$errors.failWithoutHelp("The modulemap at %s does not specify an umbrella header.", modulemapPath);
223+
}
224+
225+
return match[1];
226+
}).future<string>()();
227+
}
228+
229+
private generateFrameworkMetadata(projectRoot: string, frameworkDir: string, frameworkName: string, umbrellaHeader: string): IFuture<void> {
230+
return (() => {
231+
if (!this.$fs.exists("/usr/local/lib/libmonoboehm-2.0.1.dylib").wait()) {
232+
this.$errors.failWithoutHelp("NativeScript needs Mono 3.10 or newer installed in /usr/local");
233+
}
234+
235+
var yamlOut = path.join(frameworkDir, "Metadata");
236+
this.$fs.createDirectory(yamlOut).wait();
237+
238+
var tempHeader = path.join(yamlOut, "Metadata.h");
239+
this.$fs.writeFile(tempHeader, util.format("#import <%s/%s>", frameworkName, umbrellaHeader)).wait();
240+
241+
this.$logger.info("Generating metadata for %s.framework. This can take a minute.", frameworkName);
242+
var sdkPath = this.$childProcess.exec("xcrun -sdk iphoneos --show-sdk-path").wait().trim();
243+
// MetadataGenerator P/Invokes libclang.dylib, so we need to instruct the Mach-O loader where to find it.
244+
// Without this Mono will fail with a DllNotFoundException.
245+
// Once the MetadataGenerator is rewritten in C++ and starts linking Clang statically, this will become superfluous.
246+
var generatorExecOptions = {
247+
env: {
248+
DYLD_FALLBACK_LIBRARY_PATH: this.$childProcess.exec("xcode-select -p").wait().trim() + "/Toolchains/XcodeDefault.xctoolchain/usr/lib"
249+
}
250+
};
251+
this.$childProcess.exec(util.format('%s/Metadata/MetadataGenerator -s %s -u %s -o %s -cflags="-F%s"', projectRoot, sdkPath, tempHeader, yamlOut, frameworkDir), generatorExecOptions).wait();
252+
253+
var metadataFileName = frameworkName + ".yaml";
254+
this.$fs.copyFile(path.join(yamlOut, "Metadata-armv7", metadataFileName), path.join(projectRoot, "Metadata", "Metadata-armv7", metadataFileName)).wait();
255+
this.$fs.copyFile(path.join(yamlOut, "Metadata-arm64", metadataFileName), path.join(projectRoot, "Metadata", "Metadata-arm64", metadataFileName)).wait();
256+
257+
this.$fs.deleteDirectory(yamlOut).wait();
258+
}).future<void>()();
259+
}
260+
172261
private replaceFileContent(file: string): IFuture<void> {
173262
return (() => {
174263
var fileContent = this.$fs.readText(file).wait();
@@ -186,4 +275,4 @@ class IOSProjectService implements IPlatformProjectService {
186275
}).future<void>()();
187276
}
188277
}
189-
$injector.register("iOSProjectService", IOSProjectService);
278+
$injector.register("iOSProjectService", IOSProjectService);

lib/services/platform-service.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -322,6 +322,17 @@ export class PlatformService implements IPlatformService {
322322
}
323323
}
324324

325+
public addLibrary(platform: string, libraryPath: string): IFuture<void> {
326+
return (() => {
327+
if (!this.$fs.exists(libraryPath).wait()) {
328+
this.$errors.fail("The path %s does not exist", libraryPath);
329+
} else {
330+
var platformData = this.$platformsData.getPlatformData(platform);
331+
platformData.platformProjectService.addLibrary(platformData, libraryPath).wait();
332+
}
333+
}).future<void>()();
334+
}
335+
325336
private isValidPlatform(platform: string) {
326337
return this.$platformsData.getPlatformData(platform);
327338
}

0 commit comments

Comments
 (0)