1 /** 2 Representing a full project, with a root Package and several dependencies. 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.project; 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.generators.generator; 21 22 23 // todo: cleanup imports. 24 import std.algorithm; 25 import std.array; 26 import std.conv; 27 import std.datetime; 28 import std.exception; 29 import std.file; 30 import std.process; 31 import std.string; 32 import std.typecons; 33 import std.zip; 34 35 36 /// Representing a full project, with a root Package and several dependencies. 37 class Project { 38 private { 39 bool m_fixedPackage; 40 Path m_root; 41 PackageManager m_packageManager; 42 Json m_json; 43 Package m_main; 44 //Package[string] m_packages; 45 Package[] m_dependencies; 46 Package[][Package] m_dependees; 47 } 48 49 this(PackageManager package_manager, Path project_path) 50 { 51 m_packageManager = package_manager; 52 m_root = project_path; 53 m_fixedPackage = false; 54 m_json = Json.emptyObject; 55 reinit(); 56 } 57 58 this(PackageManager package_manager, Package pack) 59 { 60 m_packageManager = package_manager; 61 m_root = pack.path; 62 m_main = pack; 63 m_fixedPackage = true; 64 m_json = Json.emptyObject; 65 reinit(); 66 } 67 68 /// Gathers information 69 @property string info() 70 const { 71 if(!m_main) 72 return "-Unrecognized application in '"~m_root.toNativeString()~"' (probably no package.json in this directory)"; 73 string s = "-Application identifier: " ~ m_main.name; 74 s ~= "\n" ~ m_main.generateInfoString(); 75 s ~= "\n-Retrieved dependencies:"; 76 foreach(p; m_dependencies) 77 s ~= "\n" ~ p.generateInfoString(); 78 return s; 79 } 80 81 /// Gets all retrieved packages as a "packageId" = "version" associative array 82 @property string[string] cachedPackagesIDs() const { 83 string[string] pkgs; 84 foreach(p; m_dependencies) 85 pkgs[p.name] = p.vers; 86 return pkgs; 87 } 88 89 /// List of retrieved dependency Packages 90 @property const(Package[]) dependencies() const { return m_dependencies; } 91 92 /// Main package. 93 @property inout(Package) mainPackage() inout { return m_main; } 94 95 /** Allows iteration of the dependency tree in topological order 96 */ 97 int delegate(int delegate(ref const Package)) getTopologicalPackageList(bool children_first = false, in Package root_package = null, string[string] configs = null) 98 const { 99 const(Package) rootpack = root_package ? root_package : m_main; 100 101 int iterator(int delegate(ref const Package) del) 102 { 103 int ret = 0; 104 bool[const(Package)] visited; 105 void perform_rec(in Package p){ 106 if( p in visited ) return; 107 visited[p] = true; 108 109 if( !children_first ){ 110 ret = del(p); 111 if( ret ) return; 112 } 113 114 auto cfg = configs.get(p.name, null); 115 116 foreach (dn, dv; p.dependencies) { 117 // filter out dependencies not in the current configuration set 118 if (!p.hasDependency(dn, cfg)) continue; 119 auto dependency = getDependency(dn, dv.optional); 120 if(dependency) perform_rec(dependency); 121 if( ret ) return; 122 } 123 124 if( children_first ){ 125 ret = del(p); 126 if( ret ) return; 127 } 128 } 129 perform_rec(rootpack); 130 return ret; 131 } 132 133 return &iterator; 134 } 135 136 inout(Package) getDependency(string name, bool isOptional) 137 inout { 138 foreach(dp; m_dependencies) 139 if( dp.name == name ) 140 return dp; 141 if(!isOptional) throw new Exception("Unknown dependency: "~name); 142 else return null; 143 } 144 145 string getDefaultConfiguration(BuildPlatform platform, bool allow_non_library_configs = true) 146 const { 147 return m_main.getDefaultConfiguration(platform, allow_non_library_configs); 148 } 149 150 /// Rereads the applications state. 151 void reinit() 152 { 153 scope(failure){ 154 logDiagnostic("Failed to initialize project. Assuming defaults."); 155 if (!m_fixedPackage) m_main = new Package(serializeToJson(["name": "unknown"]), m_root); 156 } 157 158 m_dependencies = null; 159 m_packageManager.refresh(false); 160 161 try m_json = jsonFromFile(m_root ~ ".dub/dub.json", true); 162 catch(Exception t) logDiagnostic("Failed to read .dub/dub.json: %s", t.msg); 163 164 // load package description 165 if (!m_fixedPackage) { 166 if (!existsFile(m_root~PackageJsonFilename)) { 167 logWarn("There was no '"~PackageJsonFilename~"' found for the application in '%s'.", m_root.toNativeString()); 168 auto json = Json.emptyObject; 169 json.name = "unknown"; 170 m_main = new Package(json, m_root); 171 return; 172 } 173 174 m_main = m_packageManager.getPackage(m_root); 175 } 176 177 // some basic package lint 178 m_main.warnOnSpecialCompilerFlags(); 179 if (m_main.name != m_main.name.toLower()) { 180 logWarn(`DUB package names should always be lower case, please change to {"name": "%s"}. You can use {"targetName": "%s"} to keep the current executable name.`, 181 m_main.name.toLower(), m_main.name); 182 } 183 184 // TODO: compute the set of mutual dependencies first 185 // (i.e. ">=0.0.1 <=0.0.5" and "<= 0.0.4" get ">=0.0.1 <=0.0.4") 186 // conflicts would then also be detected. 187 void collectDependenciesRec(Package pack) 188 { 189 logDiagnostic("Collecting dependencies for %s", pack.name); 190 foreach( name, vspec; pack.dependencies ){ 191 Package p; 192 if( !vspec.path.empty ){ 193 Path path = vspec.path; 194 if( !path.absolute ) path = pack.path ~ path; 195 logDiagnostic("Adding local %s %s", path, vspec.version_); 196 p = m_packageManager.getTemporaryPackage(path, vspec.version_); 197 } else { 198 p = m_packageManager.getBestPackage(name, vspec); 199 } 200 if( !m_dependencies.canFind(p) ){ 201 logDiagnostic("Found dependency %s %s: %s", name, vspec.toString(), p !is null); 202 if( p ){ 203 m_dependencies ~= p; 204 p.warnOnSpecialCompilerFlags(); 205 collectDependenciesRec(p); 206 } 207 } 208 m_dependees[p] ~= pack; 209 //enforce(p !is null, "Failed to resolve dependency "~name~" "~vspec.toString()); 210 } 211 } 212 collectDependenciesRec(m_main); 213 } 214 215 /// Returns the applications name. 216 @property string name() const { return m_main ? m_main.name : "app"; } 217 218 @property string[] configurations() const { return m_main.configurations; } 219 220 /// Returns a map with the configuration for all packages in the dependency tree. 221 string[string] getPackageConfigs(in BuildPlatform platform, string config) 222 const { 223 struct Vertex { string pack, config; } 224 struct Edge { size_t from, to; } 225 226 Vertex[] configs; 227 Edge[] edges; 228 string[][string] parents; 229 parents[m_main.name] = null; 230 foreach (p; getTopologicalPackageList()) 231 foreach (d; p.dependencies.byKey) 232 parents[d] ~= p.name; 233 234 235 size_t createConfig(string pack, string config) { 236 foreach (i, v; configs) 237 if (v.pack == pack && v.config == config) 238 return i; 239 configs ~= Vertex(pack, config); 240 return configs.length-1; 241 } 242 243 size_t createEdge(size_t from, size_t to) { 244 auto idx = edges.countUntil(Edge(from, to)); 245 if (idx >= 0) return idx; 246 edges ~= Edge(from, to); 247 return edges.length-1; 248 } 249 250 void removeConfig(size_t i) { 251 logDebug("Eliminating config %s for %s", configs[i].config, configs[i].pack); 252 configs = configs.remove(i); 253 edges = edges.filter!(e => e.from != i && e.to != i).array(); 254 foreach (ref e; edges) { 255 if (e.from > i) e.from--; 256 if (e.to > i) e.to--; 257 } 258 } 259 260 bool isReachable(string pack, string conf) { 261 if (pack == configs[0].pack && configs[0].config == conf) return true; 262 foreach (e; edges) 263 if (configs[e.to].pack == pack && configs[e.to].config == conf) 264 return true; 265 return false; 266 //return (pack == configs[0].pack && conf == configs[0].config) || edges.canFind!(e => configs[e.to].pack == pack && configs[e.to].config == config); 267 } 268 269 bool isReachableByAllParentPacks(size_t cidx) { 270 bool[string] r; 271 foreach (p; parents[configs[cidx].pack]) r[p] = false; 272 foreach (e; edges) { 273 if (e.to != cidx) continue; 274 if (auto pp = configs[e.from].pack in r) *pp = true; 275 } 276 foreach (bool v; r) if (!v) return false; 277 return true; 278 } 279 280 // create a graph of all possible package configurations (package, config) -> (subpackage, subconfig) 281 void determineAllConfigs(in Package p) 282 { 283 foreach (c; p.getPlatformConfigurations(platform, p is m_main)) { 284 if (!isReachable(p.name, c)) { 285 //foreach (e; edges) logDebug(" %s %s -> %s %s", configs[e.from].pack, configs[e.from].config, configs[e.to].pack, configs[e.to].config); 286 logDebug("Skipping %s %s", p.name, c); 287 continue; 288 } 289 size_t cidx = createConfig(p.name, c); 290 foreach (dn; p.dependencies.byKey) { 291 auto dp = getDependency(dn, true); 292 if (!dp) continue; 293 auto subconf = p.getSubConfiguration(c, dp, platform); 294 if (subconf.empty) { 295 foreach (sc; dp.getPlatformConfigurations(platform)) { 296 logDebug("Including %s %s -> %s %s", p.name, c, dn, sc); 297 createEdge(cidx, createConfig(dn, sc)); 298 } 299 } else { 300 logDebug("Including %s %s -> %s %s", p.name, c, dn, subconf); 301 createEdge(cidx, createConfig(dn, subconf)); 302 } 303 } 304 foreach (dn; p.dependencies.byKey) { 305 auto dp = getDependency(dn, true); 306 if (!dp) continue; 307 determineAllConfigs(dp); 308 } 309 } 310 } 311 createConfig(m_main.name, config); 312 determineAllConfigs(m_main); 313 314 // successively remove configurations until only one configuration per package is left 315 bool changed; 316 do { 317 // remove all configs that are not reachable by all parent packages 318 changed = false; 319 for (size_t i = 0; i < configs.length; ) { 320 if (!isReachableByAllParentPacks(i)) { 321 removeConfig(i); 322 changed = true; 323 } else i++; 324 } 325 326 // when all edges are cleaned up, pick one package and remove all but one config 327 if (!changed) { 328 foreach (p; getTopologicalPackageList()) { 329 size_t cnt = 0; 330 for (size_t i = 0; i < configs.length; ) { 331 if (configs[i].pack == p.name) { 332 if (++cnt > 1) removeConfig(i); 333 else i++; 334 } else i++; 335 } 336 if (cnt > 1) { 337 changed = true; 338 break; 339 } 340 } 341 } 342 } while (changed); 343 344 // print out the resulting tree 345 foreach (e; edges) logDebug(" %s %s -> %s %s", configs[e.from].pack, configs[e.from].config, configs[e.to].pack, configs[e.to].config); 346 347 // return the resulting configuration set as an AA 348 string[string] ret; 349 foreach (c; configs) { 350 assert(ret.get(c.pack, c.config) == c.config, format("Conflicting configurations for %s found: %s vs. %s", c.pack, c.config, ret[c.pack])); 351 logDebug("Using configuration '%s' for %s", c.config, c.pack); 352 ret[c.pack] = c.config; 353 } 354 355 // check for conflicts (packages missing in the final configuration graph) 356 foreach (p; getTopologicalPackageList()) 357 enforce(p.name in ret, "Could not resolve configuration for package "~p.name); 358 359 return ret; 360 } 361 362 /** 363 * Fills dst with values from this project. 364 * 365 * dst gets initialized according to the given platform and config. 366 * 367 * Params: 368 * dst = The BuildSettings struct to fill with data. 369 * platform = The platform to retrieve the values for. 370 * config = Values of the given configuration will be retrieved. 371 * root_package = If non null, use it instead of the project's real root package. 372 * shallow = If true, collects only build settings for the main package (including inherited settings) and doesn't stop on target type none and sourceLibrary. 373 */ 374 void addBuildSettings(ref BuildSettings dst, in BuildPlatform platform, string config, in Package root_package = null, bool shallow = false) 375 const { 376 auto configs = getPackageConfigs(platform, config); 377 378 foreach (pkg; this.getTopologicalPackageList(false, root_package, configs)) { 379 auto pkg_path = pkg.path.toNativeString(); 380 dst.addVersions(["Have_" ~ stripDlangSpecialChars(pkg.name)]); 381 382 assert(pkg.name in configs, "Missing configuration for "~pkg.name); 383 logDebug("Gathering build settings for %s (%s)", pkg.name, configs[pkg.name]); 384 385 auto psettings = pkg.getBuildSettings(platform, configs[pkg.name]); 386 if (psettings.targetType != TargetType.none) { 387 if (shallow && pkg !is m_main) 388 psettings.sourceFiles = null; 389 processVars(dst, pkg_path, psettings); 390 if (psettings.importPaths.empty) 391 logWarn(`Package %s (configuration "%s") defines no import paths, use {"importPaths": [...]} or the default package directory structure to fix this.`, pkg.name, configs[pkg.name]); 392 if (psettings.mainSourceFile.empty && pkg is m_main && psettings.targetType == TargetType.executable) 393 logWarn(`Executable configuration "%s" of package %s defines no main source file, this may cause certain build modes to fail. Add an explicit "mainSourceFile" to the package description to fix this.`, configs[pkg.name], pkg.name); 394 } 395 if (pkg is m_main) { 396 if (!shallow) { 397 enforce(psettings.targetType != TargetType.none, "Main package has target type \"none\" - stopping build."); 398 enforce(psettings.targetType != TargetType.sourceLibrary, "Main package has target type \"sourceLibrary\" which generates no target - stopping build."); 399 } 400 dst.targetType = psettings.targetType; 401 dst.targetPath = psettings.targetPath; 402 dst.targetName = psettings.targetName; 403 dst.workingDirectory = processVars(psettings.workingDirectory, pkg_path, true); 404 if (psettings.mainSourceFile.length) 405 dst.mainSourceFile = processVars(psettings.mainSourceFile, pkg_path, true); 406 } 407 } 408 409 // always add all version identifiers of all packages 410 foreach (pkg; this.getTopologicalPackageList(false, null, configs)) { 411 auto psettings = pkg.getBuildSettings(platform, configs[pkg.name]); 412 dst.addVersions(psettings.versions); 413 } 414 } 415 416 void addBuildTypeSettings(ref BuildSettings dst, in BuildPlatform platform, string build_type) 417 { 418 bool usedefflags = !(dst.requirements & BuildRequirements.noDefaultFlags); 419 if (usedefflags) { 420 BuildSettings btsettings; 421 m_main.addBuildTypeSettings(btsettings, platform, build_type); 422 processVars(dst, m_main.path.toNativeString(), btsettings); 423 } 424 } 425 426 /// Determines if the given dependency is already indirectly referenced by other dependencies of pack. 427 bool isRedundantDependency(in Package pack, in Package dependency) 428 const { 429 foreach (dep; pack.dependencies.byKey) { 430 auto dp = getDependency(dep, true); 431 if (!dp) continue; 432 if (dp is dependency) continue; 433 foreach (ddp; getTopologicalPackageList(false, dp)) 434 if (ddp is dependency) return true; 435 } 436 return false; 437 } 438 439 440 /// Actions which can be performed to update the application. 441 Action[] determineActions(PackageSupplier[] packageSuppliers, UpdateOptions option) 442 { 443 scope(exit) writeDubJson(); 444 445 if(!m_main) { 446 Action[] a; 447 return a; 448 } 449 450 auto graph = new DependencyGraph(m_main); 451 if(!gatherMissingDependencies(packageSuppliers, graph) || graph.missing().length > 0) { 452 // Check the conflicts first. 453 auto conflicts = graph.conflicted(); 454 if(conflicts.length > 0) { 455 logError("The dependency graph could not be filled, there are conflicts."); 456 Action[] actions; 457 foreach( string pkg, dbp; graph.conflicted()) 458 actions ~= Action.conflict(pkg, dbp.dependency, dbp.packages); 459 460 // Missing dependencies could have some bogus results, therefore 461 // return only the conflicts. 462 return actions; 463 } 464 465 // Then check unresolved dependencies. 466 logError("The dependency graph could not be filled, there are unresolved dependencies."); 467 Action[] actions; 468 foreach( string pkg, rdp; graph.missing()) 469 actions ~= Action.failure(pkg, rdp.dependency, rdp.packages); 470 471 return actions; 472 } 473 474 // Gather retrieved 475 Package[string] retrieved; 476 retrieved[m_main.name] = m_main; 477 foreach(ref Package p; m_dependencies) { 478 auto pbase = p.basePackage; 479 auto pexist = retrieved.get(pbase.name, null); 480 if (pexist && pexist !is pbase){ 481 logError("The same package is referenced in different paths:"); 482 logError(" %s %s: %s", pexist.name, pexist.vers, pexist.path.toNativeString()); 483 logError(" %s %s: %s", pbase.name, pbase.vers, pbase.path.toNativeString()); 484 throw new Exception("Conflicting package multi-references."); 485 } 486 retrieved[pbase.name] = pbase; 487 } 488 489 // Check against package list and add retrieval actions 490 Action[] actions; 491 void addAction(Action act) { 492 if (!actions.canFind!(a => a.type == act.type && a.location == act.location && a.packageId == act.packageId && a.vers == act.vers)) 493 actions ~= act; 494 } 495 int[string] upgradePackages; 496 foreach( string pkg, d; graph.needed() ) { 497 auto basepkg = pkg.getBasePackage(); 498 auto p = basepkg in retrieved; 499 // TODO: auto update to latest head revision 500 if(!p || (!d.dependency.matches(p.vers) && !d.dependency.matches(Version.MASTER))) { 501 if(!p) logDiagnostic("Triggering retrieval of required package '"~basepkg~"', which was not present."); 502 else logDiagnostic("Triggering retrieval of required package '"~basepkg~"', which doesn't match the required versionh. Required '%s', available '%s'.", d.dependency, p.vers); 503 addAction(Action.get(basepkg, PlacementLocation.userWide, d.dependency, d.packages)); 504 } else { 505 if (option & UpdateOptions.upgrade) { 506 auto existing = m_packageManager.getBestPackage(basepkg, d.dependency); 507 // Only add one upgrade action for each package. 508 if(basepkg !in upgradePackages && m_packageManager.isManagedPackage(existing)) { 509 logDiagnostic("Required package '"~basepkg~"' found with version '"~p.vers~"', upgrading."); 510 upgradePackages[basepkg] = 1; 511 addAction(Action.get(basepkg, PlacementLocation.userWide, d.dependency, d.packages)); 512 } 513 } 514 else { 515 logDiagnostic("Required package '"~basepkg~"' found with version '"~p.vers~"'"); 516 } 517 } 518 } 519 520 return actions; 521 } 522 523 /// Outputs a JSON description of the project, including its deoendencies. 524 void describe(ref Json dst, BuildPlatform platform, string config) 525 { 526 dst.mainPackage = m_main.name; 527 528 auto configs = getPackageConfigs(platform, config); 529 530 auto mp = Json.emptyObject; 531 m_main.describe(mp, platform, config); 532 dst.packages = Json([mp]); 533 534 foreach (dep; m_dependencies) { 535 auto dp = Json.emptyObject; 536 dep.describe(dp, platform, configs[dep.name]); 537 dst.packages = dst.packages.get!(Json[]) ~ dp; 538 } 539 } 540 541 private bool gatherMissingDependencies(PackageSupplier[] packageSuppliers, DependencyGraph graph) 542 { 543 RequestedDependency[string] missing = graph.missing(); 544 RequestedDependency[string] oldMissing; 545 while( missing.length > 0 ) { 546 logDebug("Try to resolve %s", missing.keys); 547 if( missing.keys == oldMissing.keys ){ // FIXME: should actually compare the complete AA here 548 bool different = false; 549 foreach(string pkg, reqDep; missing) { 550 auto o = pkg in oldMissing; 551 if(o && reqDep.dependency != o.dependency) { 552 different = true; 553 break; 554 } 555 } 556 if(!different) { 557 logWarn("Could not resolve dependencies"); 558 return false; 559 } 560 } 561 562 oldMissing = missing.dup; 563 logDebug("There are %s packages missing.", missing.length); 564 565 auto toLookup = missing; 566 foreach(id, dep; graph.optional()) { 567 assert(id !in toLookup, "A missing dependency in the graph seems to be optional, which is an error."); 568 toLookup[id] = dep; 569 } 570 571 foreach(string pkg, reqDep; toLookup) { 572 if(!reqDep.dependency.valid()) { 573 logDebug("Dependency to "~pkg~" is invalid. Trying to fix by modifying others."); 574 continue; 575 } 576 577 auto ppath = pkg.getSubPackagePath(); 578 579 // TODO: auto update and update interval by time 580 logDebug("Adding package to graph: "~pkg); 581 Package p = m_packageManager.getBestPackage(pkg, reqDep.dependency); 582 if( p ) logDebug("Found present package %s %s", pkg, p.ver); 583 584 // Don't bother with not available optional packages. 585 if( !p && reqDep.dependency.optional ) continue; 586 587 // Try an already present package first 588 if( p && needsUpToDateCheck(p) ){ 589 logInfo("Triggering update of package %s", pkg); 590 p = null; 591 } 592 593 if( !p ){ 594 try { 595 logDiagnostic("Fetching package %s (%d suppliers registered)", pkg, packageSuppliers.length); 596 foreach (ps; packageSuppliers) { 597 try { 598 auto sp = new Package(ps.getPackageDescription(ppath[0], reqDep.dependency, false)); 599 foreach (spn; ppath[1 .. $]) 600 sp = sp.getSubPackage(spn); 601 p = sp; 602 break; 603 } catch (Exception e) { 604 logDiagnostic("No metadata for %s: %s", ps.description, e.msg); 605 } 606 } 607 enforce(p !is null, "Could not find package candidate for "~pkg~" "~reqDep.dependency.toString()); 608 markUpToDate(ppath[0]); 609 } 610 catch(Throwable e) { 611 logError("Failed to retrieve metadata for package %s: %s", pkg, e.msg); 612 logDiagnostic("Full error: %s", e.toString().sanitize()); 613 } 614 } 615 616 if(p) 617 graph.insert(p); 618 } 619 graph.clearUnused(); 620 621 // As the dependencies are filled in starting from the outermost 622 // packages, resolving those conflicts won't happen (?). 623 if(graph.conflicted().length > 0) { 624 logInfo("There are conflicts in the dependency graph."); 625 return false; 626 } 627 628 missing = graph.missing(); 629 } 630 631 return true; 632 } 633 634 private bool needsUpToDateCheck(Package pack) { 635 version (none) { // needs to be updated for the new package system (where no project local packages exist) 636 try { 637 auto time = m_json["dub"]["lastUpdate"].opt!(Json[string]).get(pack.name, Json("")).get!string; 638 if( !time.length ) return true; 639 return (Clock.currTime() - SysTime.fromISOExtString(time)) > dur!"days"(1); 640 } catch(Exception t) return true; 641 } else return false; 642 } 643 644 private void markUpToDate(string packageId) { 645 logDebug("markUpToDate(%s)", packageId); 646 Json create(ref Json json, string object) { 647 if( object !in json ) json[object] = Json.emptyObject; 648 return json[object]; 649 } 650 create(m_json, "dub"); 651 create(m_json["dub"], "lastUpdate"); 652 m_json["dub"]["lastUpdate"][packageId] = Json( Clock.currTime().toISOExtString() ); 653 654 writeDubJson(); 655 } 656 657 private void writeDubJson() { 658 // don't bother to write an empty file 659 if( m_json.length == 0 ) return; 660 661 try { 662 logDebug("writeDubJson"); 663 auto dubpath = m_root~".dub"; 664 if( !exists(dubpath.toNativeString()) ) mkdir(dubpath.toNativeString()); 665 auto dstFile = openFile((dubpath~"dub.json").toString(), FileMode.CreateTrunc); 666 scope(exit) dstFile.close(); 667 dstFile.writePrettyJsonString(m_json); 668 } catch( Exception e ){ 669 logWarn("Could not write .dub/dub.json."); 670 } 671 } 672 } 673 674 /// Actions to be performed by the dub 675 struct Action { 676 enum Type { 677 fetch, 678 remove, 679 conflict, 680 failure 681 } 682 683 immutable { 684 Type type; 685 string packageId; 686 PlacementLocation location; 687 Dependency vers; 688 } 689 const Package pack; 690 const Dependency[string] issuer; 691 692 static Action get(string pkg, PlacementLocation location, in Dependency dep, Dependency[string] context) 693 { 694 return Action(Type.fetch, pkg, location, dep, context); 695 } 696 697 static Action remove(Package pkg, Dependency[string] context) 698 { 699 return Action(Type.remove, pkg, context); 700 } 701 702 static Action conflict(string pkg, in Dependency dep, Dependency[string] context) 703 { 704 return Action(Type.conflict, pkg, PlacementLocation.userWide, dep, context); 705 } 706 707 static Action failure(string pkg, in Dependency dep, Dependency[string] context) 708 { 709 return Action(Type.failure, pkg, PlacementLocation.userWide, dep, context); 710 } 711 712 private this(Type id, string pkg, PlacementLocation location, in Dependency d, Dependency[string] issue) 713 { 714 this.type = id; 715 this.packageId = pkg; 716 this.location = location; 717 this.vers = d; 718 this.issuer = issue; 719 } 720 721 private this(Type id, Package pkg, Dependency[string] issue) 722 { 723 pack = pkg; 724 type = id; 725 packageId = pkg.name; 726 vers = cast(immutable)Dependency(pkg.ver); 727 issuer = issue; 728 } 729 730 string toString() const { 731 return to!string(type) ~ ": " ~ packageId ~ ", " ~ to!string(vers); 732 } 733 } 734 735 736 enum UpdateOptions 737 { 738 none = 0, 739 upgrade = 1<<1, 740 preRelease = 1<<2 // inclde pre-release versions in upgrade 741 } 742 743 744 /// Indicates where a package has been or should be placed to. 745 enum PlacementLocation { 746 /// Packages retrived with 'local' will be placed in the current folder 747 /// using the package name as destination. 748 local, 749 /// Packages with 'userWide' will be placed in a folder accessible by 750 /// all of the applications from the current user. 751 userWide, 752 /// Packages retrieved with 'systemWide' will be placed in a shared folder, 753 /// which can be accessed by all users of the system. 754 systemWide 755 } 756 757 private void processVars(ref BuildSettings dst, string project_path, BuildSettings settings) 758 { 759 dst.addDFlags(processVars(project_path, settings.dflags)); 760 dst.addLFlags(processVars(project_path, settings.lflags)); 761 dst.addLibs(processVars(project_path, settings.libs)); 762 dst.addSourceFiles(processVars(project_path, settings.sourceFiles, true)); 763 dst.addImportFiles(processVars(project_path, settings.importFiles, true)); 764 dst.addStringImportFiles(processVars(project_path, settings.stringImportFiles, true)); 765 dst.addCopyFiles(processVars(project_path, settings.copyFiles, true)); 766 dst.addVersions(processVars(project_path, settings.versions)); 767 dst.addDebugVersions(processVars(project_path, settings.debugVersions)); 768 dst.addImportPaths(processVars(project_path, settings.importPaths, true)); 769 dst.addStringImportPaths(processVars(project_path, settings.stringImportPaths, true)); 770 dst.addPreGenerateCommands(processVars(project_path, settings.preGenerateCommands)); 771 dst.addPostGenerateCommands(processVars(project_path, settings.postGenerateCommands)); 772 dst.addPreBuildCommands(processVars(project_path, settings.preBuildCommands)); 773 dst.addPostBuildCommands(processVars(project_path, settings.postBuildCommands)); 774 dst.addRequirements(settings.requirements); 775 dst.addOptions(settings.options); 776 } 777 778 private string[] processVars(string project_path, string[] vars, bool are_paths = false) 779 { 780 auto ret = appender!(string[])(); 781 processVars(ret, project_path, vars, are_paths); 782 return ret.data; 783 784 } 785 private void processVars(ref Appender!(string[]) dst, string project_path, string[] vars, bool are_paths = false) 786 { 787 foreach (var; vars) dst.put(processVars(var, project_path, are_paths)); 788 } 789 790 private string processVars(string var, string project_path, bool is_path) 791 { 792 auto idx = std..string.indexOf(var, '$'); 793 if (idx >= 0) { 794 auto vres = appender!string(); 795 while (idx >= 0) { 796 if (idx+1 >= var.length) break; 797 if (var[idx+1] == '$') { 798 vres.put(var[0 .. idx+1]); 799 var = var[idx+2 .. $]; 800 } else { 801 vres.put(var[0 .. idx]); 802 var = var[idx+1 .. $]; 803 804 size_t idx2 = 0; 805 while( idx2 < var.length && isIdentChar(var[idx2]) ) idx2++; 806 auto varname = var[0 .. idx2]; 807 var = var[idx2 .. $]; 808 809 string env_variable; 810 if( varname == "PACKAGE_DIR" ) vres.put(project_path); 811 else if( (env_variable = environment.get(varname)) != null) vres.put(env_variable); 812 else enforce(false, "Invalid variable: "~varname); 813 } 814 idx = std..string.indexOf(var, '$'); 815 } 816 vres.put(var); 817 var = vres.data; 818 } 819 if (is_path) { 820 auto p = Path(var); 821 if (!p.absolute) { 822 logDebug("Fixing relative path: %s ~ %s", project_path, p.toNativeString()); 823 p = Path(project_path) ~ p; 824 } 825 return p.toNativeString(); 826 } else return var; 827 } 828 829 private bool isIdentChar(dchar ch) 830 { 831 return ch >= 'A' && ch <= 'Z' || ch >= 'a' && ch <= 'z' || ch >= '0' && ch <= '9' || ch == '_'; 832 } 833 834 private string stripDlangSpecialChars(string s) 835 { 836 import std.array; 837 import std.uni; 838 auto ret = appender!string(); 839 foreach(ch; s) 840 ret.put(isIdentChar(ch) ? ch : '_'); 841 return ret.data; 842 }