Skip to content

Implement lockfile and symlink option #140

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Dec 4, 2014
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions lib/bootstrap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,5 +33,6 @@ $injector.requireCommand("emulate", "./commands/emulate");
$injector.requireCommand("list-devices", "./commands/list-devices");

$injector.require("npm", "./node-package-manager");
$injector.require("lockfile", "./lockfile");
$injector.require("config", "./config");
$injector.require("optionsService", "./services/options-service");
6 changes: 5 additions & 1 deletion lib/declarations.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
interface INodePackageManager {
getCacheRootPath(): IFuture<string>;
getCacheRootPath(): string;
addToCache(packageName: string, version: string): IFuture<void>;
cacheUnpack(packageName: string, version: string, unpackTarget?: string): IFuture<void>;
load(config?: any): IFuture<void>;
Expand All @@ -21,3 +21,7 @@ interface IApplicationPackage {
time: Date;
}

interface ILockFile {
lock(): IFuture<void>;
unlock(): IFuture<void>;
}
15 changes: 15 additions & 0 deletions lib/definitions/lockfile.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
declare module "lockfile" {
export function lock(lockFilename: string, lockParams: ILockParams, callback: (err: Error) => void): void;
export function lockSync(lockFilename: string, lockParams: ILockSyncParams): void;
export function unlock(lockFilename: string, callback: (err: Error) => void): void;
export function unlockSync(lockFilename: string): void;

interface ILockSyncParams {
retries: number;
stale: number;
}

interface ILockParams extends ILockSyncParams {
retryWait: number;
}
}
42 changes: 42 additions & 0 deletions lib/lockfile.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
///<reference path=".d.ts"/>
"use strict";

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"use strict";

import Future = require("fibers/future");
import lockfile = require("lockfile");
import path = require("path");
import options = require("./options");

export class LockFile implements ILockFile {
private static LOCK_FILENAME = path.join(options["profile-dir"], ".lock");
private static LOCK_EXPIRY_PERIOD_SEC = 180;
private static LOCK_PARAMS = {
retryWait: 100,
retries: LockFile.LOCK_EXPIRY_PERIOD_SEC*10,
stale: LockFile.LOCK_EXPIRY_PERIOD_SEC*1000
};

public lock(): IFuture<void> {
var future = new Future<void>();
lockfile.lock(LockFile.LOCK_FILENAME, LockFile.LOCK_PARAMS, (err: Error) => {
if(err) {
future.throw(err);
} else {
future.return();
}
});
return future;
}

public unlock(): IFuture<void> {
var future = new Future<void>();
lockfile.unlock(LockFile.LOCK_FILENAME, (err: Error) => {
if(err) {
future.throw(err);
} else {
future.return();
}
});
return future;
}
}
$injector.register("lockfile", LockFile);
1 change: 0 additions & 1 deletion lib/nativescript-cli.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
///<reference path=".d.ts"/>
"use strict";

import path = require("path");

require("./bootstrap");
Expand Down
96 changes: 51 additions & 45 deletions lib/node-package-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,28 +14,22 @@ export class NodePackageManager implements INodePackageManager {
private static NPM_REGISTRY_URL = "http://registry.npmjs.org/";

private versionsCache: IDictionary<string[]>;
private isLoaded: boolean;

constructor(private $logger: ILogger,
private $errors: IErrors,
private $httpClient: Server.IHttpClient,
private $staticConfig: IStaticConfig,
private $fs: IFileSystem) {
private $fs: IFileSystem,
private $lockfile: ILockFile) {
this.versionsCache = {};
this.load().wait();
}

public getCacheRootPath(): IFuture<string> {
return (() => {
this.load().wait();
return npm.cache;
}).future<string>()();
public getCacheRootPath(): string {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why not a property?

return npm.cache;
}

public addToCache(packageName: string, version: string): IFuture<void> {
return (() => {
this.load().wait();
this.addToCacheCore(packageName, version).wait();
}).future<void>()();
return this.addToCacheCore(packageName, version);
}

public load(config?: any): IFuture<void> {
Expand All @@ -52,29 +46,21 @@ export class NodePackageManager implements INodePackageManager {

public install(packageName: string, opts?: INpmInstallOptions): IFuture<string> {
return (() => {
try {
this.load().wait(); // It's obligatory to execute load before whatever npm function
this.$lockfile.lock().wait();

try {
var packageToInstall = packageName;
var pathToSave = (opts && opts.pathToSave) || npm.cache;
var version = (opts && opts.version) || null;
var isSemanticVersioningDisabled = options.frameworkPath ? true : false; // We need to disable sem versioning for local packages

if(version) {
this.validateVersion(packageName, version).wait();
packageToInstall = packageName + "@" + version;
}

this.installCore(packageToInstall, pathToSave, isSemanticVersioningDisabled).wait();
return this.installCore(packageToInstall, pathToSave, version).wait();
} catch(error) {
this.$logger.debug(error);
this.$errors.fail(NodePackageManager.NPM_LOAD_FAILED);
this.$errors.fail("%s. Error: %s", NodePackageManager.NPM_LOAD_FAILED, error);
} finally {
this.$lockfile.unlock().wait();
}

var pathToNodeModules = path.join(pathToSave, "node_modules");
var folders = this.$fs.readDirectory(pathToNodeModules).wait();
return path.join(pathToNodeModules, folders[0]);

}).future<string>()();
}

Expand All @@ -86,21 +72,39 @@ export class NodePackageManager implements INodePackageManager {
}).future<string>()();
}

private installCore(packageName: string, pathToSave: string, isSemanticVersioningDisabled: boolean): IFuture<void> {
var currentVersion = this.$staticConfig.version;
if(!semver.valid(currentVersion)) {
this.$errors.fail("Invalid version.");
}
private installCore(packageName: string, pathToSave: string, version: string): IFuture<string> {
return (() => {
if (options.frameworkPath) {
if (this.$fs.getFsStats(options.frameworkPath).wait().isFile()) {
this.npmInstall(packageName, pathToSave, version).wait();
var pathToNodeModules = path.join(pathToSave, "node_modules");
var folders = this.$fs.readDirectory(pathToNodeModules).wait();
return path.join(pathToNodeModules, folders[0]);
}
return options.frameworkPath;
} else {
var version = version || this.getLatestVersion(packageName).wait();
var packagePath = path.join(npm.cache, packageName, version, "package");
if (!this.isPackageCached(packagePath).wait()) {
this.addToCacheCore(packageName, version).wait();
}

if(!isSemanticVersioningDisabled) {
var incrementedVersion = semver.inc(currentVersion, constants.ReleaseType.MINOR);
if(packageName.indexOf("@") < 0) {
packageName = packageName + "@<" + incrementedVersion;
if(!this.isPackageUnpacked(packagePath).wait()) {
this.cacheUnpack(packageName, version).wait();
}
return packagePath;
}
}
}).future<string>()();
}

private npmInstall(packageName: string, pathToSave: string, version: string): IFuture<void> {
this.$logger.out("Installing ", packageName);

var incrementedVersion = semver.inc(version, constants.ReleaseType.MINOR);
if (!options.frameworkPath && packageName.indexOf("@") < 0) {
packageName = packageName + "@<" + incrementedVersion;
}

var future = new Future<void>();
npm.commands["install"](pathToSave, packageName, (err: Error, data: any) => {
if(err) {
Expand All @@ -113,6 +117,17 @@ export class NodePackageManager implements INodePackageManager {
return future;
}

private isPackageCached(packagePath: string): IFuture<boolean> {
return this.$fs.exists(packagePath);
}

private isPackageUnpacked(packagePath: string): IFuture<boolean> {
return (() => {
return this.$fs.getFsStats(packagePath).wait().isDirectory() &&
helpers.enumerateFilesInDirectorySync(packagePath).length > 1;
}).future<boolean>()();
}

private addToCacheCore(packageName: string, version: string): IFuture<void> {
var future = new Future<void>();
npm.commands["cache"].add(packageName, version, undefined, (err: Error, data: any) => {
Expand Down Expand Up @@ -150,14 +165,5 @@ export class NodePackageManager implements INodePackageManager {
return this.versionsCache[packageName];
}).future<string[]>()();
}

private validateVersion(packageName: string, version: string): IFuture<void> {
return (() => {
var versions = this.getAvailableVersions(packageName).wait();
if(!_.contains(versions, version)) {
this.$errors.fail("Invalid version. Valid versions are: %s", helpers.formatListOfNames(versions, "and"));
}
}).future<void>()();
}
}
$injector.register("npm", NodePackageManager);
1 change: 1 addition & 0 deletions lib/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ var knownOpts:any = {
"release": Boolean,
"device": Boolean,
"emulator": Boolean,
"symlink": Boolean,
"keyStorePath": String,
"keyStorePassword": String,
"keyStoreAlias": String,
Expand Down
40 changes: 36 additions & 4 deletions lib/services/android-project-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,13 +47,20 @@ class AndroidProjectService implements IPlatformProjectService {

public createProject(projectRoot: string, frameworkDir: string): IFuture<void> {
return (() => {
this.$fs.ensureDirectoryExists(projectRoot).wait();

this.validateAndroidTarget(frameworkDir); // We need framework to be installed to validate android target so we can't call this method in validate()

var paths = "assets libs res".split(' ').map(p => path.join(frameworkDir, p));
shell.cp("-R", paths, projectRoot);
if(options.symlink) {
this.copy(projectRoot, frameworkDir, "res", "-R").wait();
this.copy(projectRoot, frameworkDir, ".project AndroidManifest.xml project.properties", "-f").wait();

paths = ".project AndroidManifest.xml project.properties".split(' ').map(p => path.join(frameworkDir, p));
shell.cp("-f", paths, projectRoot);
this.symlinkDirectory("assets", projectRoot, frameworkDir).wait();
this.symlinkDirectory("libs", projectRoot, frameworkDir).wait();
} else {
this.copy(projectRoot, frameworkDir, "assets libs res", "-R").wait();
this.copy(projectRoot, frameworkDir, ".project AndroidManifest.xml project.properties", "-f").wait();
}

// Create src folder
var packageName = this.$projectData.projectId;
Expand Down Expand Up @@ -124,6 +131,13 @@ class AndroidProjectService implements IPlatformProjectService {
return [".jar", ".dat"];
}

private copy(projectRoot: string, frameworkDir: string, files: string, cpArg: string): IFuture<void> {
return (() => {
var paths = files.split(' ').map(p => path.join(frameworkDir, p));
shell.cp(cpArg, paths, projectRoot);
}).future<void>()();
}

private spawn(command: string, args: string[]): IFuture<void> {
if (hostInfo.isWindows()) {
args.unshift('/s', '/c', command);
Expand Down Expand Up @@ -238,5 +252,23 @@ class AndroidProjectService implements IPlatformProjectService {
}
}).future<void>()();
}

private symlinkDirectory(directoryName: string, projectRoot: string, frameworkDir: string): IFuture<void> {
return (() => {
this.$fs.createDirectory(path.join(projectRoot, directoryName)).wait();
var directoryContent = this.$fs.readDirectory(path.join(frameworkDir, directoryName)).wait();

_.each(directoryContent, (file: string) => {
var sourceFilePath = path.join(frameworkDir, directoryName, file);
var destinationFilePath = path.join(projectRoot, directoryName, file);
if(this.$fs.getFsStats(sourceFilePath).wait().isFile()) {
this.$fs.symlink(sourceFilePath, destinationFilePath).wait();
} else {
this.$fs.symlink(sourceFilePath, destinationFilePath, "dir").wait();
}
});

}).future<void>()();
}
}
$injector.register("androidProjectService", AndroidProjectService);
17 changes: 16 additions & 1 deletion lib/services/ios-project-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,22 @@ class IOSProjectService implements IPlatformProjectService {

public createProject(projectRoot: string, frameworkDir: string): IFuture<void> {
return (() => {
shell.cp("-R", path.join(frameworkDir, "*"), projectRoot);
if(options.symlink) {
this.$fs.ensureDirectoryExists(path.join(projectRoot, IOSProjectService.IOS_PROJECT_NAME_PLACEHOLDER)).wait();
var xcodeProjectName = util.format("%s.xcodeproj", IOSProjectService.IOS_PROJECT_NAME_PLACEHOLDER);

shell.cp("-R", path.join(frameworkDir, IOSProjectService.IOS_PROJECT_NAME_PLACEHOLDER, "*"), path.join(projectRoot, IOSProjectService.IOS_PROJECT_NAME_PLACEHOLDER));
shell.cp("-R", path.join(frameworkDir, xcodeProjectName), path.join(projectRoot));

var directoryContent = this.$fs.readDirectory(frameworkDir).wait();
var frameworkFiles = _.difference(directoryContent, [IOSProjectService.IOS_PROJECT_NAME_PLACEHOLDER, xcodeProjectName]);
_.each(frameworkFiles, (file: string) => {
this.$fs.symlink(path.join(frameworkDir, file), path.join(projectRoot, file)).wait();
});

} else {
shell.cp("-R", path.join(frameworkDir, "*"), projectRoot);
}
}).future<void>()();
}

Expand Down
19 changes: 9 additions & 10 deletions lib/services/platform-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,9 +91,11 @@ export class PlatformService implements IPlatformService {
platformData.platformProjectService.createProject(platformData.projectRoot, frameworkDir).wait();
var installedVersion = this.$fs.readJson(path.join(frameworkDir, "../", "package.json")).wait().version;

// Need to remove unneeded node_modules folder
// One level up is the runtime module and one above is the node_modules folder.
this.$fs.deleteDirectory(path.join(frameworkDir, "../../")).wait();
if(options.frameworkPath && !options.symlink) {
// Need to remove unneeded node_modules folder
// One level up is the runtime module and one above is the node_modules folder.
this.$fs.deleteDirectory(path.join(frameworkDir, "../../")).wait();
}

platformData.platformProjectService.interpolateData(platformData.projectRoot).wait();
platformData.platformProjectService.afterCreateProject(platformData.projectRoot).wait();
Expand Down Expand Up @@ -405,7 +407,7 @@ export class PlatformService implements IPlatformService {

// Add new framework files
var newFrameworkFiles = this.getFrameworkFiles(platformData, newVersion).wait();
var cacheDirectoryPath = this.getNpmCacheDirectoryCore(platformData.frameworkPackageName, newVersion).wait();
var cacheDirectoryPath = this.getNpmCacheDirectoryCore(platformData.frameworkPackageName, newVersion);
_.each(newFrameworkFiles, file => {
shell.cp("-f", path.join(cacheDirectoryPath, file), path.join(platformData.projectRoot, file));
});
Expand Down Expand Up @@ -433,7 +435,7 @@ export class PlatformService implements IPlatformService {

private getNpmCacheDirectory(packageName: string, version: string): IFuture<string> {
return (() => {
var npmCacheDirectoryPath = this.getNpmCacheDirectoryCore(packageName, version).wait();
var npmCacheDirectoryPath = this.getNpmCacheDirectoryCore(packageName, version);

if(!this.$fs.exists(npmCacheDirectoryPath).wait()) {
this.$npm.addToCache(packageName, version).wait();
Expand All @@ -443,11 +445,8 @@ export class PlatformService implements IPlatformService {
}).future<string>()();
}

private getNpmCacheDirectoryCore(packageName: string, version: string): IFuture<string> {
return (() => {
var npmCacheRoot = this.$npm.getCacheRootPath().wait();
return path.join(npmCacheRoot, packageName, version, "package");
}).future<string>()();
private getNpmCacheDirectoryCore(packageName: string, version: string): string {
return path.join(this.$npm.getCacheRootPath(), packageName, version, "package");
}
}
$injector.register("platformService", PlatformService);
Loading