1 /**
2 	A package manager.
3 
4 	Copyright: © 2012-2013 Matthias Dondorff
5 	License: Subject to the terms of the MIT license, as written in the included LICENSE.txt file.
6 	Authors: Matthias Dondorff, Sönke Ludwig
7 */
8 module dub.dub;
9 
10 import dub.compilers.compiler;
11 import dub.dependency;
12 import dub.internal.utils;
13 import dub.internal.vibecompat.core.file;
14 import dub.internal.vibecompat.core.log;
15 import dub.internal.vibecompat.data.json;
16 import dub.internal.vibecompat.inet.url;
17 import dub.package_;
18 import dub.packagemanager;
19 import dub.packagesupplier;
20 import dub.project;
21 import dub.generators.generator;
22 import dub.init;
23 
24 
25 // todo: cleanup imports.
26 import std.algorithm;
27 import std.array;
28 import std.conv;
29 import std.datetime;
30 import std.exception;
31 import std.file;
32 import std.process;
33 import std.string;
34 import std.typecons;
35 import std.zip;
36 
37 
38 
39 /// The default supplier for packages, which is the registry
40 /// hosted by code.dlang.org.
41 PackageSupplier[] defaultPackageSuppliers()
42 {
43 	Url url = Url.parse("http://code.dlang.org/");
44 	logDiagnostic("Using dub registry url '%s'", url);
45 	return [new RegistryPackageSupplier(url)];
46 }
47 
48 /// The Dub class helps in getting the applications
49 /// dependencies up and running. An instance manages one application.
50 class Dub {
51 	private {
52 		bool m_dryRun = false;
53 		PackageManager m_packageManager;
54 		PackageSupplier[] m_packageSuppliers;
55 		Path m_rootPath, m_tempPath;
56 		Path m_userDubPath, m_systemDubPath;
57 		Json m_systemConfig, m_userConfig;
58 		Path m_projectPath;
59 		Project m_project;
60 	}
61 
62 	/// Initiales the package manager for the vibe application
63 	/// under root.
64 	this(PackageSupplier[] additional_package_suppliers = null, string root_path = ".")
65 	{
66 		m_rootPath = Path(root_path);
67 		if (!m_rootPath.absolute) m_rootPath = Path(getcwd()) ~ m_rootPath;
68 
69 		version(Windows){
70 			m_systemDubPath = Path(environment.get("ProgramData")) ~ "dub/";
71 			m_userDubPath = Path(environment.get("APPDATA")) ~ "dub/";
72 			m_tempPath = Path(environment.get("TEMP"));
73 		} else version(Posix){
74 			m_systemDubPath = Path("/var/lib/dub/");
75 			m_userDubPath = Path(environment.get("HOME")) ~ ".dub/";
76 			if(!m_userDubPath.absolute)
77 				m_userDubPath = Path(getcwd()) ~ m_userDubPath;
78 			m_tempPath = Path("/tmp");
79 		}
80 		
81 		m_userConfig = jsonFromFile(m_userDubPath ~ "settings.json", true);
82 		m_systemConfig = jsonFromFile(m_systemDubPath ~ "settings.json", true);
83 
84 		PackageSupplier[] ps = additional_package_suppliers;
85 		if (auto pp = "registryUrls" in m_userConfig)
86 			ps ~= deserializeJson!(string[])(*pp)
87 				.map!(url => cast(PackageSupplier)new RegistryPackageSupplier(Url(url)))
88 				.array;
89 		if (auto pp = "registryUrls" in m_systemConfig)
90 			ps ~= deserializeJson!(string[])(*pp)
91 				.map!(url => cast(PackageSupplier)new RegistryPackageSupplier(Url(url)))
92 				.array;
93 		ps ~= defaultPackageSuppliers();
94 
95 		m_packageSuppliers = ps;
96 		m_packageManager = new PackageManager(m_userDubPath, m_systemDubPath);
97 		updatePackageSearchPath();
98 	}
99 
100 	@property void dryRun(bool v) { m_dryRun = v; }
101 
102 	/** Returns the root path (usually the current working directory).
103 	*/
104 	@property Path rootPath() const { return m_rootPath; }
105 	/// ditto
106 	@property void rootPath(Path root_path)
107 	{
108 		m_rootPath = root_path;
109 		if (!m_rootPath.absolute) m_rootPath = Path(getcwd()) ~ m_rootPath;
110 	}
111 
112 	/// Returns the name listed in the package.json of the current
113 	/// application.
114 	@property string projectName() const { return m_project.name; }
115 
116 	@property Path projectPath() const { return m_projectPath; }
117 
118 	@property string[] configurations() const { return m_project.configurations; }
119 
120 	@property inout(PackageManager) packageManager() inout { return m_packageManager; }
121 
122 	@property inout(Project) project() inout { return m_project; }
123 
124 	/// Loads the package from the current working directory as the main
125 	/// project package.
126 	void loadPackageFromCwd()
127 	{
128 		loadPackage(m_rootPath);
129 	}
130 
131 	/// Loads the package from the specified path as the main project package.
132 	void loadPackage(Path path)
133 	{
134 		m_projectPath = path;
135 		updatePackageSearchPath();
136 		m_project = new Project(m_packageManager, m_projectPath);
137 	}
138 
139 	/// Loads a specific package as the main project package (can be a sub package)
140 	void loadPackage(Package pack)
141 	{
142 		m_projectPath = pack.path;
143 		updatePackageSearchPath();
144 		m_project = new Project(m_packageManager, pack);
145 	}
146 
147 	string getDefaultConfiguration(BuildPlatform platform, bool allow_non_library_configs = true) const { return m_project.getDefaultConfiguration(platform, allow_non_library_configs); }
148 
149 	/// Performs retrieval and removal as necessary for
150 	/// the application.
151 	/// @param options bit combination of UpdateOptions
152 	void update(UpdateOptions options)
153 	{
154 		bool[string] masterVersionUpgrades;
155 		while (true) {
156 			Action[] allActions = m_project.determineActions(m_packageSuppliers, options);
157 			Action[] actions;
158 			foreach(a; allActions)
159 				if(a.packageId !in masterVersionUpgrades)
160 					actions ~= a;
161 
162 			if (actions.length == 0) break;
163 
164 			logInfo("The following changes will be performed:");
165 			bool conflictedOrFailed = false;
166 			foreach(Action a; actions) {
167 				logInfo("%s %s %s, %s", capitalize(to!string(a.type)), a.packageId, a.vers, a.location);
168 				if( a.type == Action.Type.conflict || a.type == Action.Type.failure ) {
169 					logInfo("Issued by: ");
170 					conflictedOrFailed = true;
171 					foreach(string pkg, d; a.issuer)
172 						logInfo(" "~pkg~": %s", d);
173 				}
174 			}
175 
176 			if (conflictedOrFailed || m_dryRun) return;
177 
178 			// Remove first
179 			foreach(Action a; actions.filter!(a => a.type == Action.Type.remove)) {
180 				assert(a.pack !is null, "No package specified for removal.");
181 				remove(a.pack);
182 			}
183 			foreach(Action a; actions.filter!(a => a.type == Action.Type.fetch)) {
184 				fetch(a.packageId, a.vers, a.location, (options & UpdateOptions.upgrade) != 0, (options & UpdateOptions.preRelease) != 0);
185 				// never update the same package more than once
186 				masterVersionUpgrades[a.packageId] = true;
187 			}
188 
189 			m_project.reinit();
190 		}
191 	}
192 
193 	/// Generate project files for a specified IDE.
194 	/// Any existing project files will be overridden.
195 	void generateProject(string ide, GeneratorSettings settings) {
196 		auto generator = createProjectGenerator(ide, m_project, m_packageManager);
197 		if (m_dryRun) return; // TODO: pass m_dryRun to the generator
198 		generator.generate(settings);
199 	}
200 
201 	void testProject(BuildSettings build_settings, BuildPlatform platform, string config, Path custom_main_file, string[] run_args)
202 	{
203 		if (custom_main_file.length && !custom_main_file.absolute) custom_main_file = getWorkingDirectory() ~ custom_main_file;
204 
205 		if (config.length == 0) {
206 			// if a custom main file was given, favor the first library configuration, so that it can be applied
207 			if (custom_main_file.length) config = m_project.getDefaultConfiguration(platform, false);
208 			// else look for a "unittest" configuration
209 			if (!config.length && m_project.mainPackage.configurations.canFind("unittest")) config = "unittest";
210 			// if not found, fall back to the first "library" configuration
211 			if (!config.length) config = m_project.getDefaultConfiguration(platform, false);
212 			// if still nothing found, use the first executable configuration
213 			if (!config.length) config = m_project.getDefaultConfiguration(platform, true);
214 		}
215 
216 		auto generator = createProjectGenerator("build", m_project, m_packageManager);
217 		GeneratorSettings settings;
218 		settings.platform = platform;
219 		settings.compiler = getCompiler(platform.compilerBinary);
220 		settings.buildType = "unittest";
221 		settings.buildSettings = build_settings;
222 		settings.run = true;
223 		settings.runArgs = run_args;
224 
225 		auto test_config = format("__test__%s__", config);
226 
227 		BuildSettings lbuildsettings = build_settings;
228 		m_project.addBuildSettings(lbuildsettings, platform, config, null, true);
229 		if (lbuildsettings.targetType == TargetType.none) {
230 			logInfo(`Configuration '%s' has target type "none". Skipping test.`, config);
231 			return;
232 		}
233 		
234 		if (lbuildsettings.targetType == TargetType.executable) {
235 			if (config == "unittest") logInfo("Running custom 'unittest' configuration.", config);
236 			else logInfo(`Configuration '%s' does not output a library. Falling back to "dub -b unittest -c %s".`, config, config);
237 			if (!custom_main_file.empty) logWarn("Ignoring custom main file.");
238 			settings.config = config;
239 		} else if (lbuildsettings.sourceFiles.empty) {
240 			logInfo(`No source files found in configuration '%s'. Falling back to "dub -b unittest".`, config);
241 			if (!custom_main_file.empty) logWarn("Ignoring custom main file.");
242 			settings.config = m_project.getDefaultConfiguration(platform);
243 		} else {
244 			logInfo(`Generating test runner configuration '%s' for '%s' (%s).`, test_config, config, lbuildsettings.targetType);
245 
246 			BuildSettingsTemplate tcinfo = m_project.mainPackage.info.getConfiguration(config).buildSettings;
247 			tcinfo.targetType = TargetType.executable;
248 			tcinfo.targetName = test_config;
249 			tcinfo.versions[""] ~= "VibeCustomMain"; // HACK for vibe.d's legacy main() behavior
250 			string custommodname;
251 			if (custom_main_file.length) {
252 				import std.path;
253 				tcinfo.sourceFiles[""] ~= custom_main_file.relativeTo(m_project.mainPackage.path).toNativeString();
254 				tcinfo.importPaths[""] ~= custom_main_file.parentPath.toNativeString();
255 				custommodname = custom_main_file.head.toString().baseName(".d");
256 			}
257 
258 			string[] import_modules;
259 			foreach (file; lbuildsettings.sourceFiles) {
260 				if (file.endsWith(".d") && Path(file).head.toString() != "package.d")
261 					import_modules ~= lbuildsettings.determineModuleName(Path(file), m_project.mainPackage.path);
262 			}
263 
264 			// generate main file
265 			Path mainfile = getTempDir() ~ "dub_test_root.d";
266 			tcinfo.sourceFiles[""] ~= mainfile.toNativeString();
267 			tcinfo.mainSourceFile = mainfile.toNativeString();
268 			if (!m_dryRun) {
269 				auto fil = openFile(mainfile, FileMode.CreateTrunc);
270 				scope(exit) fil.close();
271 				fil.write("module dub_test_root;\n");
272 				fil.write("import std.typetuple;\n");
273 				foreach (mod; import_modules) fil.write(format("static import %s;\n", mod));
274 				fil.write("alias allModules = TypeTuple!(");
275 				foreach (i, mod; import_modules) {
276 					if (i > 0) fil.write(", ");
277 					fil.write(mod);
278 				}
279 				fil.write(");\n");
280 				if (custommodname.length) {
281 					fil.write(format("import %s;\n", custommodname));
282 				} else {
283 					fil.write(q{
284 						import std.stdio;
285 						import core.runtime;
286 
287 						void main() { writeln("All unit tests were successful."); }
288 						shared static this() {
289 							version (Have_tested) {
290 								import tested;
291 								import core.runtime;
292 								import std.exception;
293 								Runtime.moduleUnitTester = () => true;
294 								//runUnitTests!app(new JsonTestResultWriter("results.json"));
295 								enforce(runUnitTests!allModules(new ConsoleTestResultWriter), "Unit tests failed.");
296 							}
297 						}
298 					});
299 				}
300 			}
301 			m_project.mainPackage.info.configurations ~= ConfigurationInfo(test_config, tcinfo);
302 			m_project = new Project(m_packageManager, m_project.mainPackage);
303 
304 			settings.config = test_config;
305 		}
306 
307 		generator.generate(settings);
308 	}
309 
310 	/// Outputs a JSON description of the project, including its dependencies.
311 	void describeProject(BuildPlatform platform, string config)
312 	{
313 		auto dst = Json.emptyObject;
314 		dst.configuration = config;
315 		dst.compiler = platform.compiler;
316 		dst.architecture = platform.architecture.serializeToJson();
317 		dst.platform = platform.platform.serializeToJson();
318 
319 		m_project.describe(dst, platform, config);
320 		logInfo("%s", dst.toPrettyString());
321 	}
322 
323 
324 	/// Returns all cached  packages as a "packageId" = "version" associative array
325 	string[string] cachedPackages() const { return m_project.cachedPackagesIDs(); }
326 
327 	/// Fetches the package matching the dependency and places it in the specified location.
328 	Package fetch(string packageId, const Dependency dep, PlacementLocation location, bool force_branch_upgrade, bool use_prerelease)
329 	{
330 		Json pinfo;
331 		PackageSupplier supplier;
332 		foreach(ps; m_packageSuppliers){
333 			try {
334 				pinfo = ps.getPackageDescription(packageId, dep, use_prerelease);
335 				supplier = ps;
336 				break;
337 			} catch(Exception e) {
338 				logDiagnostic("Package %s not found at for %s: %s", packageId, ps.description(), e.msg);
339 				logDebug("Full error: %s", e.toString().sanitize());
340 			}
341 		}
342 		enforce(pinfo.type != Json.Type.undefined, "No package "~packageId~" was found matching the dependency "~dep.toString());
343 		string ver = pinfo["version"].get!string;
344 
345 		Path placement;
346 		final switch (location) {
347 			case PlacementLocation.local: placement = m_rootPath; break;
348 			case PlacementLocation.userWide: placement = m_userDubPath ~ "packages/"; break;
349 			case PlacementLocation.systemWide: placement = m_systemDubPath ~ "packages/"; break;
350 		}
351 
352 		// always upgrade branch based versions - TODO: actually check if there is a new commit available
353 		if (auto pack = m_packageManager.getPackage(packageId, ver, placement)) {
354 			if (!ver.startsWith("~") || !force_branch_upgrade || location == PlacementLocation.local) {
355 				// TODO: support git working trees by performing a "git pull" instead of this
356 				logInfo("Package %s %s (%s) is already present with the latest version, skipping upgrade.",
357 					packageId, ver, placement);
358 				return pack;
359 			} else {
360 				logInfo("Removing present package of %s %s", packageId, ver);
361 				if (!m_dryRun) m_packageManager.remove(pack);
362 			}
363 		}
364 
365 		logInfo("Fetching %s %s...", packageId, ver);
366 		if (m_dryRun) return null;
367 
368 		logDiagnostic("Acquiring package zip file");
369 		auto dload = m_projectPath ~ ".dub/temp/downloads";
370 		auto tempfname = packageId ~ "-" ~ (ver.startsWith('~') ? ver[1 .. $] : ver) ~ ".zip";
371 		auto tempFile = m_tempPath ~ tempfname;
372 		string sTempFile = tempFile.toNativeString();
373 		if (exists(sTempFile)) std.file.remove(sTempFile);
374 		supplier.retrievePackage(tempFile, packageId, dep, use_prerelease); // Q: continue on fail?
375 		scope(exit) std.file.remove(sTempFile);
376 
377 		logInfo("Placing %s %s to %s...", packageId, ver, placement.toNativeString());
378 		auto clean_package_version = ver[ver.startsWith("~") ? 1 : 0 .. $];
379 		Path dstpath = placement ~ (packageId ~ "-" ~ clean_package_version);
380 
381 		return m_packageManager.storeFetchedPackage(tempFile, pinfo, dstpath);
382 	}
383 
384 	/// Removes a given package from the list of present/cached modules.
385 	/// @removeFromApplication: if true, this will also remove an entry in the
386 	/// list of dependencies in the application's package.json
387 	void remove(in Package pack)
388 	{
389 		logInfo("Removing %s in %s", pack.name, pack.path.toNativeString());
390 		if (!m_dryRun) m_packageManager.remove(pack);
391 	}
392 
393 	/// @see remove(string, string, RemoveLocation)
394 	enum RemoveVersionWildcard = "*";
395 
396 	/// This will remove a given package with a specified version from the 
397 	/// location.
398 	/// It will remove at most one package, unless @param version_ is 
399 	/// specified as wildcard "*". 
400 	/// @param package_id Package to be removed
401 	/// @param version_ Identifying a version or a wild card. An empty string
402 	/// may be passed into. In this case the package will be removed from the
403 	/// location, if there is only one version retrieved. This will throw an
404 	/// exception, if there are multiple versions retrieved.
405 	/// Note: as wildcard string only "*" is supported.
406 	/// @param location_
407 	void remove(string package_id, string version_, PlacementLocation location_)
408 	{
409 		enforce(!package_id.empty);
410 		if (location_ == PlacementLocation.local) {
411 			logInfo("To remove a locally placed package, make sure you don't have any data"
412 					~ "\nleft in it's directory and then simply remove the whole directory.");
413 			return;
414 		}
415 
416 		Package[] packages;
417 		const bool wildcardOrEmpty = version_ == RemoveVersionWildcard || version_.empty;
418 
419 		// Use package manager
420 		foreach(pack; m_packageManager.getPackageIterator(package_id)) {
421 			if( wildcardOrEmpty || pack.vers == version_ ) {
422 				packages ~= pack;
423 			}
424 		}
425 
426 		if(packages.empty) {
427 			logError("Cannot find package to remove. (id:%s, version:%s, location:%s)", package_id, version_, location_);
428 			return;
429 		}
430 
431 		if(version_.empty && packages.length > 1) {
432 			logError("Cannot remove package '%s', there multiple possibilities at location '%s'.", package_id, location_);
433 			logError("Retrieved versions:");
434 			foreach(pack; packages) 
435 				logError(to!string(pack.vers()));
436 			throw new Exception("Failed to remove package.");
437 		}
438 
439 		logDebug("Removing %s packages.", packages.length);
440 		foreach(pack; packages) {
441 			try {
442 				remove(pack);
443 				logInfo("Removing %s, version %s.", package_id, pack.vers);
444 			}
445 			catch logError("Failed to remove %s, version %s. Continuing with other packages (if any).", package_id, pack.vers);
446 		}
447 	}
448 
449 	void addLocalPackage(string path, string ver, bool system)
450 	{
451 		if (m_dryRun) return;
452 		m_packageManager.addLocalPackage(makeAbsolute(path), ver, system ? LocalPackageType.system : LocalPackageType.user);
453 	}
454 
455 	void removeLocalPackage(string path, bool system)
456 	{
457 		if (m_dryRun) return;
458 		m_packageManager.removeLocalPackage(makeAbsolute(path), system ? LocalPackageType.system : LocalPackageType.user);
459 	}
460 
461 	void addSearchPath(string path, bool system)
462 	{
463 		if (m_dryRun) return;
464 		m_packageManager.addSearchPath(makeAbsolute(path), system ? LocalPackageType.system : LocalPackageType.user);
465 	}
466 
467 	void removeSearchPath(string path, bool system)
468 	{
469 		if (m_dryRun) return;
470 		m_packageManager.removeSearchPath(makeAbsolute(path), system ? LocalPackageType.system : LocalPackageType.user);
471 	}
472 
473 	void createEmptyPackage(Path path, string type)
474 	{
475 		if( !path.absolute() ) path = m_rootPath ~ path;
476 		path.normalize();
477 
478 		if (m_dryRun) return;
479 
480 		initPackage(path, type);
481 
482 		//Act smug to the user. 
483 		logInfo("Successfully created an empty project in '%s'.", path.toNativeString());
484 	}
485 
486 	void runDdox(bool run)
487 	{
488 		if (m_dryRun) return;
489 
490 		auto ddox_pack = m_packageManager.getBestPackage("ddox", ">=0.0.0");
491 		if (!ddox_pack) ddox_pack = m_packageManager.getBestPackage("ddox", "~master");
492 		if (!ddox_pack) {
493 			logInfo("DDOX is not present, getting it and storing user wide");
494 			ddox_pack = fetch("ddox", Dependency(">=0.0.0"), PlacementLocation.userWide, false, false);
495 		}
496 
497 		version(Windows) auto ddox_exe = "ddox.exe";
498 		else auto ddox_exe = "ddox";
499 
500 		if( !existsFile(ddox_pack.path~ddox_exe) ){
501 			logInfo("DDOX in %s is not built, performing build now.", ddox_pack.path.toNativeString());
502 
503 			auto ddox_dub = new Dub(m_packageSuppliers);
504 			ddox_dub.loadPackage(ddox_pack.path);
505 
506 			auto compiler_binary = "dmd";
507 
508 			GeneratorSettings settings;
509 			settings.config = "application";
510 			settings.compiler = getCompiler(compiler_binary);
511 			settings.platform = settings.compiler.determinePlatform(settings.buildSettings, compiler_binary);
512 			settings.buildType = "debug";
513 			ddox_dub.generateProject("build", settings);
514 
515 			//runCommands(["cd "~ddox_pack.path.toNativeString()~" && dub build -v"]);
516 		}
517 
518 		auto p = ddox_pack.path;
519 		p.endsWithSlash = true;
520 		auto dub_path = p.toNativeString();
521 
522 		string[] commands;
523 		string[] filterargs = m_project.mainPackage.info.ddoxFilterArgs.dup;
524 		if (filterargs.empty) filterargs = ["--min-protection=Protected", "--only-documented"];
525 		commands ~= dub_path~"ddox filter "~filterargs.join(" ")~" docs.json";
526 		if (!run) {
527 			commands ~= dub_path~"ddox generate-html --navigation-type=ModuleTree docs.json docs";
528 			version(Windows) commands ~= "xcopy /S /D "~dub_path~"public\\* docs\\";
529 			else commands ~= "cp -ru \""~dub_path~"public\"/* docs/";
530 		}
531 		runCommands(commands);
532 
533 		if (run) {
534 			spawnProcess([dub_path~"ddox", "serve-html", "--navigation-type=ModuleTree", "docs.json", "--web-file-dir="~dub_path~"public"]);
535 			browse("http://127.0.0.1:8080/");
536 		}
537 	}
538 
539 	private void updatePackageSearchPath()
540 	{
541 		auto p = environment.get("DUBPATH");
542 		Path[] paths;
543 
544 		version(Windows) enum pathsep = ";";
545 		else enum pathsep = ":";
546 		if (p.length) paths ~= p.split(pathsep).map!(p => Path(p))().array();
547 		m_packageManager.searchPath = paths;
548 	}
549 
550 	private Path makeAbsolute(Path p) const { return p.absolute ? p : m_rootPath ~ p; }
551 	private Path makeAbsolute(string p) const { return makeAbsolute(Path(p)); }
552 }
553 
554 string determineModuleName(BuildSettings settings, Path file, Path base_path)
555 {
556 	assert(base_path.absolute);
557 	if (!file.absolute) file = base_path ~ file;
558 
559 	size_t path_skip = 0;
560 	foreach (ipath; settings.importPaths.map!(p => Path(p))) {
561 		if (!ipath.absolute) ipath = base_path ~ ipath;
562 		assert(!ipath.empty);
563 		if (file.startsWith(ipath) && ipath.length > path_skip)
564 			path_skip = ipath.length;
565 	}
566 
567 	enforce(path_skip > 0,
568 		format("Source file '%s' not found in any import path.", file.toNativeString()));
569 
570 	auto mpath = file[path_skip .. file.length];
571 	auto ret = appender!string;
572 	foreach (i; 0 .. mpath.length) {
573 		import std.path;
574 		auto p = mpath[i].toString();
575 		if (p == "package.d") break;
576 		if (i > 0) ret ~= ".";
577 		if (i+1 < mpath.length) ret ~= p;
578 		else ret ~= p.baseName(".d");
579 	}
580 	return ret.data;
581 }