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 }