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.std.process;
13 import dub.internal.utils;
14 import dub.internal.vibecompat.core.file;
15 import dub.internal.vibecompat.core.log;
16 import dub.internal.vibecompat.data.json;
17 import dub.internal.vibecompat.inet.url;
18 import dub.package_;
19 import dub.packagemanager;
20 import dub.packagesupplier;
21 import dub.project;
22 import dub.generators.generator;
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.string;
33 import std.typecons;
34 import std.zip;
35 
36 
37 
38 /// The default supplier for packages, which is the registry
39 /// hosted by code.dlang.org.
40 PackageSupplier[] defaultPackageSuppliers()
41 {
42 	Url url = Url.parse("http://code.dlang.org/");
43 	logDiagnostic("Using dub registry url '%s'", url);
44 	return [new RegistryPackageSupplier(url)];
45 }
46 
47 /// The Dub class helps in getting the applications
48 /// dependencies up and running. An instance manages one application.
49 class Dub {
50 	private {
51 		PackageManager m_packageManager;
52 		PackageSupplier[] m_packageSuppliers;
53 		Path m_rootPath, m_tempPath;
54 		Path m_userDubPath, m_systemDubPath;
55 		Json m_systemConfig, m_userConfig;
56 		Path m_projectPath;
57 		Project m_project;
58 	}
59 
60 	/// Initiales the package manager for the vibe application
61 	/// under root.
62 	this(PackageSupplier[] additional_package_suppliers = null, string root_path = ".")
63 	{
64 		m_rootPath = Path(root_path);
65 		if (!m_rootPath.absolute) m_rootPath = Path(getcwd()) ~ m_rootPath;
66 
67 		version(Windows){
68 			m_systemDubPath = Path(environment.get("ProgramData")) ~ "dub/";
69 			m_userDubPath = Path(environment.get("APPDATA")) ~ "dub/";
70 			m_tempPath = Path(environment.get("TEMP"));
71 		} else version(Posix){
72 			m_systemDubPath = Path("/var/lib/dub/");
73 			m_userDubPath = Path(environment.get("HOME")) ~ ".dub/";
74 			m_tempPath = Path("/tmp");
75 		}
76 		
77 		m_userConfig = jsonFromFile(m_userDubPath ~ "settings.json", true);
78 		m_systemConfig = jsonFromFile(m_systemDubPath ~ "settings.json", true);
79 
80 		PackageSupplier[] ps = additional_package_suppliers;
81 		if (auto pp = "registryUrls" in m_userConfig) ps ~= deserializeJson!(string[])(*pp).map!(url => new RegistryPackageSupplier(Url(url))).array;
82 		if (auto pp = "registryUrls" in m_systemConfig) ps ~= deserializeJson!(string[])(*pp).map!(url => new RegistryPackageSupplier(Url(url))).array;
83 		ps ~= defaultPackageSuppliers();
84 
85 		m_packageSuppliers = ps;
86 		m_packageManager = new PackageManager(m_userDubPath, m_systemDubPath);
87 		updatePackageSearchPath();
88 	}
89 
90 	/** Returns the root path (usually the current working directory).
91 	*/
92 	@property Path rootPath() const { return m_rootPath; }
93 
94 	/// Returns the name listed in the package.json of the current
95 	/// application.
96 	@property string projectName() const { return m_project.name; }
97 
98 	@property Path projectPath() const { return m_projectPath; }
99 
100 	@property string[] configurations() const { return m_project.configurations; }
101 
102 	@property inout(PackageManager) packageManager() inout { return m_packageManager; }
103 
104 	/// Loads the package from the current working directory as the main
105 	/// project package.
106 	void loadPackageFromCwd()
107 	{
108 		loadPackage(m_rootPath);
109 	}
110 
111 	/// Loads the package from the specified path as the main project package.
112 	void loadPackage(Path path)
113 	{
114 		m_projectPath = path;
115 		updatePackageSearchPath();
116 		m_project = new Project(m_packageManager, m_projectPath);
117 	}
118 
119 	/// Loads a specific package as the main project package (can be a sub package)
120 	void loadPackage(Package pack)
121 	{
122 		m_projectPath = pack.path;
123 		updatePackageSearchPath();
124 		m_project = new Project(m_packageManager, pack);
125 	}
126 
127 	string getDefaultConfiguration(BuildPlatform platform) const { return m_project.getDefaultConfiguration(platform); }
128 
129 	/// Performs installation and uninstallation as necessary for
130 	/// the application.
131 	/// @param options bit combination of UpdateOptions
132 	void update(UpdateOptions options)
133 	{
134 		bool[string] masterVersionUpgrades;
135 		while (true) {
136 			Action[] allActions = m_project.determineActions(m_packageSuppliers, options);
137 			Action[] actions;
138 			foreach(a; allActions)
139 				if(a.packageId !in masterVersionUpgrades)
140 					actions ~= a;
141 
142 			if (actions.length == 0) break;
143 
144 			logInfo("The following changes will be performed:");
145 			bool conflictedOrFailed = false;
146 			foreach(Action a; actions) {
147 				logInfo("%s %s %s, %s", capitalize(to!string(a.type)), a.packageId, a.vers, a.location);
148 				if( a.type == Action.Type.conflict || a.type == Action.Type.failure ) {
149 					logInfo("Issued by: ");
150 					conflictedOrFailed = true;
151 					foreach(string pkg, d; a.issuer)
152 						logInfo(" "~pkg~": %s", d);
153 				}
154 			}
155 
156 			if (conflictedOrFailed || options & UpdateOptions.JustAnnotate) return;
157 
158 			// Uninstall first
159 			foreach(Action a; filter!((Action a) => a.type == Action.Type.uninstall)(actions)) {
160 				assert(a.pack !is null, "No package specified for uninstall.");
161 				uninstall(a.pack);
162 			}
163 			foreach(Action a; filter!((Action a) => a.type == Action.Type.install)(actions)) {
164 				install(a.packageId, a.vers, a.location, (options & UpdateOptions.Upgrade) != 0);
165 				// never update the same package more than once
166 				masterVersionUpgrades[a.packageId] = true;
167 			}
168 
169 			m_project.reinit();
170 		}
171 	}
172 
173 	/// Generate project files for a specified IDE.
174 	/// Any existing project files will be overridden.
175 	void generateProject(string ide, GeneratorSettings settings) {
176 		auto generator = createProjectGenerator(ide, m_project, m_packageManager);
177 		generator.generateProject(settings);
178 	}
179 
180 	/// Outputs a JSON description of the project, including its dependencies.
181 	void describeProject(BuildPlatform platform, string config)
182 	{
183 		auto dst = Json.EmptyObject;
184 		dst.configuration = config;
185 		dst.compiler = platform.compiler;
186 		dst.architecture = platform.architecture.serializeToJson();
187 		dst.platform = platform.platform.serializeToJson();
188 
189 		m_project.describe(dst, platform, config);
190 		logInfo("%s", dst.toPrettyString());
191 	}
192 
193 
194 	/// Gets all installed packages as a "packageId" = "version" associative array
195 	string[string] installedPackages() const { return m_project.installedPackagesIDs(); }
196 
197 	/// Installs the package matching the dependency into the application.
198 	Package install(string packageId, const Dependency dep, InstallLocation location, bool force_branch_upgrade)
199 	{
200 		Json pinfo;
201 		PackageSupplier supplier;
202 		foreach(ps; m_packageSuppliers){
203 			try {
204 				pinfo = ps.getPackageDescription(packageId, dep);
205 				supplier = ps;
206 				break;
207 			} catch(Exception) {}
208 		}
209 		enforce(pinfo.type != Json.Type.Undefined, "No package "~packageId~" was found matching the dependency "~dep.toString());
210 		string ver = pinfo["version"].get!string;
211 
212 		Path install_path;
213 		final switch (location) {
214 			case InstallLocation.local: install_path = m_rootPath; break;
215 			case InstallLocation.userWide: install_path = m_userDubPath ~ "packages/"; break;
216 			case InstallLocation.systemWide: install_path = m_systemDubPath ~ "packages/"; break;
217 		}
218 
219 		// always upgrade branch based versions - TODO: actually check if there is a new commit available
220 		if (auto pack = m_packageManager.getPackage(packageId, ver, install_path)) {
221 			if (!ver.startsWith("~") || !force_branch_upgrade || location == InstallLocation.local) {
222 				// TODO: support git working trees by performing a "git pull" instead of this
223 				logInfo("Package %s %s (%s) is already installed with the latest version, skipping upgrade.",
224 					packageId, ver, install_path);
225 				return pack;
226 			} else {
227 				logInfo("Removing current installation of %s %s", packageId, ver);
228 				m_packageManager.uninstall(pack);
229 			}
230 		}
231 
232 		logInfo("Downloading %s %s...", packageId, ver);
233 
234 		logDiagnostic("Acquiring package zip file");
235 		auto dload = m_projectPath ~ ".dub/temp/downloads";
236 		auto tempfname = packageId ~ "-" ~ (ver.startsWith('~') ? ver[1 .. $] : ver) ~ ".zip";
237 		auto tempFile = m_tempPath ~ tempfname;
238 		string sTempFile = tempFile.toNativeString();
239 		if(exists(sTempFile)) remove(sTempFile);
240 		supplier.retrievePackage(tempFile, packageId, dep); // Q: continue on fail?
241 		scope(exit) remove(sTempFile);
242 
243 		logInfo("Installing %s %s to %s...", packageId, ver, install_path.toNativeString());
244 		auto clean_package_version = ver[ver.startsWith("~") ? 1 : 0 .. $];
245 		Path dstpath = install_path ~ (packageId ~ "-" ~ clean_package_version);
246 
247 		return m_packageManager.install(tempFile, pinfo, dstpath);
248 	}
249 
250 	/// Uninstalls a given package from the list of installed modules.
251 	/// @removeFromApplication: if true, this will also remove an entry in the
252 	/// list of dependencies in the application's package.json
253 	void uninstall(in Package pack)
254 	{
255 		logInfo("Uninstalling %s in %s", pack.name, pack.path.toNativeString());
256 		m_packageManager.uninstall(pack);
257 	}
258 
259 	/// @see uninstall(string, string, InstallLocation)
260 	enum UninstallVersionWildcard = "*";
261 
262 	/// This will uninstall a given package with a specified version from the 
263 	/// location.
264 	/// It will remove at most one package, unless @param version_ is 
265 	/// specified as wildcard "*". 
266 	/// @param package_id Package to be removed
267 	/// @param version_ Identifying a version or a wild card. An empty string
268 	/// may be passed into. In this case the package will be removed from the
269 	/// location, if there is only one version installed. This will throw an
270 	/// exception, if there are multiple versions installed.
271 	/// Note: as wildcard string only "*" is supported.
272 	/// @param location_
273 	void uninstall(string package_id, string version_, InstallLocation location_) {
274 		enforce(!package_id.empty);
275 		if(location_ == InstallLocation.local) {
276 			logInfo("To uninstall a locally installed package, make sure you don't have any data"
277 					~ "\nleft in it's directory and then simply remove the whole directory.");
278 			return;
279 		}
280 
281 		Package[] packages;
282 		const bool wildcardOrEmpty = version_ == UninstallVersionWildcard || version_.empty;
283 
284 		// Use package manager
285 		foreach(pack; m_packageManager.getPackageIterator(package_id)) {
286 			if( wildcardOrEmpty || pack.vers == version_ ) {
287 				packages ~= pack;
288 			}
289 		}
290 
291 		if(packages.empty) {
292 			logError("Cannot find package to uninstall. (id:%s, version:%s, location:%s)", package_id, version_, location_);
293 			return;
294 		}
295 
296 		if(version_.empty && packages.length > 1) {
297 			logError("Cannot uninstall package '%s', there multiple possibilities at location '%s'.", package_id, location_);
298 			logError("Installed versions:");
299 			foreach(pack; packages) 
300 				logError(to!string(pack.vers()));
301 			throw new Exception("Failed to uninstall package.");
302 		}
303 
304 		logDebug("Uninstalling %s packages.", packages.length);
305 		foreach(pack; packages) {
306 			try {
307 				uninstall(pack);
308 				logInfo("Uninstalled %s, version %s.", package_id, pack.vers);
309 			}
310 			catch logError("Failed to uninstall %s, version %s. Continuing with other packages (if any).", package_id, pack.vers);
311 		}
312 	}
313 
314 	void addLocalPackage(string path, string ver, bool system)
315 	{
316 		m_packageManager.addLocalPackage(makeAbsolute(path), Version(ver), system ? LocalPackageType.system : LocalPackageType.user);
317 	}
318 
319 	void removeLocalPackage(string path, bool system)
320 	{
321 		m_packageManager.removeLocalPackage(makeAbsolute(path), system ? LocalPackageType.system : LocalPackageType.user);
322 	}
323 
324 	void addSearchPath(string path, bool system)
325 	{
326 		m_packageManager.addSearchPath(makeAbsolute(path), system ? LocalPackageType.system : LocalPackageType.user);
327 	}
328 
329 	void removeSearchPath(string path, bool system)
330 	{
331 		m_packageManager.removeSearchPath(makeAbsolute(path), system ? LocalPackageType.system : LocalPackageType.user);
332 	}
333 
334 	void createEmptyPackage(Path path)
335 	{
336 		if( !path.absolute() ) path = m_rootPath ~ path;
337 		path.normalize();
338 
339 		//Check to see if a target directory needs to be created
340 		if( !path.empty ){
341 			if( !existsFile(path) )
342 				createDirectory(path);
343 		} 
344 
345 		//Make sure we do not overwrite anything accidentally
346 		if( existsFile(path ~ PackageJsonFilename) ||
347 			existsFile(path ~ "source") ||
348 			existsFile(path ~ "views") ||
349 			existsFile(path ~ "public") )
350 		{
351 			throw new Exception("The current directory is not empty.\n");
352 		}
353 
354 		//raw strings must be unindented. 
355 		immutable packageJson = 
356 `{
357 	"name": "`~(path.empty ? "my-project" : path.head.toString().toLower())~`",
358 	"description": "An example project skeleton",
359 	"homepage": "http://example.org",
360 	"copyright": "Copyright © 2000, Your Name",
361 	"authors": [
362 		"Your Name"
363 	],
364 	"dependencies": {
365 	}
366 }
367 `;
368 		immutable appFile =
369 `import std.stdio;
370 
371 void main()
372 { 
373 	writeln("Edit source/app.d to start your project.");
374 }
375 `;
376 
377 		//Create the common directories.
378 		createDirectory(path ~ "source");
379 		createDirectory(path ~ "views");
380 		createDirectory(path ~ "public");
381 
382 		//Create the common files. 
383 		openFile(path ~ PackageJsonFilename, FileMode.Append).write(packageJson);
384 		openFile(path ~ "source/app.d", FileMode.Append).write(appFile);     
385 
386 		//Act smug to the user. 
387 		logInfo("Successfully created an empty project in '"~path.toNativeString()~"'.");
388 	}
389 
390 	void runDdox()
391 	{
392 		auto ddox_pack = m_packageManager.getBestPackage("ddox", ">=0.0.0");
393 		if (!ddox_pack) ddox_pack = m_packageManager.getBestPackage("ddox", "~master");
394 		if (!ddox_pack) {
395 			logInfo("DDOX is not installed, performing user wide installation.");
396 			ddox_pack = install("ddox", Dependency(">=0.0.0"), InstallLocation.userWide, false);
397 		}
398 
399 		version(Windows) auto ddox_exe = "ddox.exe";
400 		else auto ddox_exe = "ddox";
401 
402 		if( !existsFile(ddox_pack.path~ddox_exe) ){
403 			logInfo("DDOX in %s is not built, performing build now.", ddox_pack.path.toNativeString());
404 
405 			auto ddox_dub = new Dub(m_packageSuppliers);
406 			ddox_dub.loadPackage(ddox_pack.path);
407 
408 			GeneratorSettings settings;
409 			settings.config = "application";
410 			settings.compiler = getCompiler(settings.platform.compilerBinary);
411 			settings.platform = settings.compiler.determinePlatform(settings.buildSettings, settings.platform.compilerBinary);
412 			settings.buildType = "debug";
413 			ddox_dub.generateProject("build", settings);
414 
415 			//runCommands(["cd "~ddox_pack.path.toNativeString()~" && dub build -v"]);
416 		}
417 
418 		auto p = ddox_pack.path;
419 		p.endsWithSlash = true;
420 		auto dub_path = p.toNativeString();
421 
422 		string[] commands;
423 		string[] filterargs = m_project.mainPackage.info.ddoxFilterArgs.dup;
424 		if (filterargs.empty) filterargs = ["--min-protection=Protected", "--only-documented"];
425 		commands ~= dub_path~"ddox filter "~filterargs.join(" ")~" docs.json";
426 		commands ~= dub_path~"ddox generate-html --navigation-type=ModuleTree docs.json docs";
427 		version(Windows) commands ~= "xcopy /S /D \""~dub_path~"public\\*\" docs\\";
428 		else commands ~= "cp -r \""~dub_path~"public/*\" docs/";
429 		runCommands(commands);
430 	}
431 
432 	private void updatePackageSearchPath()
433 	{
434 		auto p = environment.get("DUBPATH");
435 		Path[] paths;
436 
437 		version(Windows) enum pathsep = ":";
438 		else enum pathsep = ";";
439 		if (p.length) paths ~= p.split(pathsep).map!(p => Path(p))().array();
440 		m_packageManager.searchPath = paths;
441 	}
442 
443 	private Path makeAbsolute(Path p) const { return p.absolute ? p : m_rootPath ~ p; }
444 	private Path makeAbsolute(string p) const { return makeAbsolute(Path(p)); }
445 }