1 /** 2 Abstract representation of a package description file. 3 4 Copyright: © 2012-2014 rejectedsoftware e.K. 5 License: Subject to the terms of the MIT license, as written in the included LICENSE.txt file. 6 Authors: Sönke Ludwig, Matthias Dondorff 7 */ 8 module dub.recipe.packagerecipe; 9 10 import dub.compilers.compiler; 11 import dub.compilers.utils : warnOnSpecialCompilerFlags; 12 import dub.dependency; 13 14 import dub.internal.vibecompat.core.file; 15 import dub.internal.vibecompat.core.log; 16 import dub.internal.vibecompat.inet.path; 17 18 import std.algorithm : findSplit, sort; 19 import std.array : join, split; 20 import std.exception : enforce; 21 import std.file; 22 import std.range; 23 import std.process : environment; 24 25 26 /** 27 Returns the individual parts of a qualified package name. 28 29 Sub qualified package names are lists of package names separated by ":". For 30 example, "packa:packb:packc" references a package named "packc" that is a 31 sub package of "packb", which in turn is a sub package of "packa". 32 */ 33 string[] getSubPackagePath(string package_name) @safe pure 34 { 35 return package_name.split(":"); 36 } 37 38 /** 39 Returns the name of the top level package for a given (sub) package name. 40 41 In case of a top level package, the qualified name is returned unmodified. 42 */ 43 string getBasePackageName(string package_name) @safe pure 44 { 45 return package_name.findSplit(":")[0]; 46 } 47 48 /** 49 Returns the qualified sub package part of the given package name. 50 51 This is the part of the package name excluding the base package 52 name. See also $(D getBasePackageName). 53 */ 54 string getSubPackageName(string package_name) @safe pure 55 { 56 return package_name.findSplit(":")[2]; 57 } 58 59 @safe unittest 60 { 61 assert(getSubPackagePath("packa:packb:packc") == ["packa", "packb", "packc"]); 62 assert(getSubPackagePath("pack") == ["pack"]); 63 assert(getBasePackageName("packa:packb:packc") == "packa"); 64 assert(getBasePackageName("pack") == "pack"); 65 assert(getSubPackageName("packa:packb:packc") == "packb:packc"); 66 assert(getSubPackageName("pack") == ""); 67 } 68 69 /** 70 Represents the contents of a package recipe file (dub.json/dub.sdl) in an abstract way. 71 72 This structure is used to reason about package descriptions in isolation. 73 For higher level package handling, see the $(D Package) class. 74 */ 75 struct PackageRecipe { 76 string name; 77 string version_; 78 string description; 79 string homepage; 80 string[] authors; 81 string copyright; 82 string license; 83 string[] ddoxFilterArgs; 84 string ddoxTool; 85 BuildSettingsTemplate buildSettings; 86 ConfigurationInfo[] configurations; 87 BuildSettingsTemplate[string] buildTypes; 88 89 ToolchainRequirements toolchainRequirements; 90 91 SubPackage[] subPackages; 92 93 inout(ConfigurationInfo) getConfiguration(string name) 94 inout { 95 foreach (c; configurations) 96 if (c.name == name) 97 return c; 98 throw new Exception("Unknown configuration: "~name); 99 } 100 101 /** Clones the package recipe recursively. 102 */ 103 PackageRecipe clone() const { return .clone(this); } 104 } 105 106 struct SubPackage 107 { 108 string path; 109 PackageRecipe recipe; 110 } 111 112 /// Describes minimal toolchain requirements 113 struct ToolchainRequirements 114 { 115 import std.typecons : Tuple, tuple; 116 117 /// DUB version requirement 118 Dependency dub = Dependency.any; 119 /// D front-end version requirement 120 Dependency frontend = Dependency.any; 121 /// DMD version requirement 122 Dependency dmd = Dependency.any; 123 /// LDC version requirement 124 Dependency ldc = Dependency.any; 125 /// GDC version requirement 126 Dependency gdc = Dependency.any; 127 128 /** Get the list of supported compilers. 129 130 Returns: 131 An array of couples of compiler name and compiler requirement 132 */ 133 @property Tuple!(string, Dependency)[] supportedCompilers() const 134 { 135 Tuple!(string, Dependency)[] res; 136 if (dmd != Dependency.invalid) res ~= Tuple!(string, Dependency)("dmd", dmd); 137 if (ldc != Dependency.invalid) res ~= Tuple!(string, Dependency)("ldc", ldc); 138 if (gdc != Dependency.invalid) res ~= Tuple!(string, Dependency)("gdc", gdc); 139 return res; 140 } 141 142 bool empty() 143 const { 144 import std.algorithm.searching : all; 145 return only(dub, frontend, dmd, ldc, gdc) 146 .all!(r => r == Dependency.any); 147 } 148 } 149 150 151 /// Bundles information about a build configuration. 152 struct ConfigurationInfo { 153 string name; 154 string[] platforms; 155 BuildSettingsTemplate buildSettings; 156 157 this(string name, BuildSettingsTemplate build_settings) 158 { 159 enforce(!name.empty, "Configuration name is empty."); 160 this.name = name; 161 this.buildSettings = build_settings; 162 } 163 164 bool matchesPlatform(in BuildPlatform platform) 165 const { 166 if( platforms.empty ) return true; 167 foreach(p; platforms) 168 if( platform.matchesSpecification("-"~p) ) 169 return true; 170 return false; 171 } 172 } 173 174 /// This keeps general information about how to build a package. 175 /// It contains functions to create a specific BuildSetting, targeted at 176 /// a certain BuildPlatform. 177 struct BuildSettingsTemplate { 178 Dependency[string] dependencies; 179 BuildSettingsTemplate[string] dependencyBuildSettings; 180 string systemDependencies; 181 TargetType targetType = TargetType.autodetect; 182 string targetPath; 183 string targetName; 184 string workingDirectory; 185 string mainSourceFile; 186 string[string] subConfigurations; 187 string[][string] dflags; 188 string[][string] lflags; 189 string[][string] libs; 190 string[][string] sourceFiles; 191 string[][string] sourcePaths; 192 string[][string] excludedSourceFiles; 193 string[][string] copyFiles; 194 string[][string] extraDependencyFiles; 195 string[][string] versions; 196 string[][string] debugVersions; 197 string[][string] versionFilters; 198 string[][string] debugVersionFilters; 199 string[][string] importPaths; 200 string[][string] stringImportPaths; 201 string[][string] preGenerateCommands; 202 string[][string] postGenerateCommands; 203 string[][string] preBuildCommands; 204 string[][string] postBuildCommands; 205 string[][string] preRunCommands; 206 string[][string] postRunCommands; 207 BuildRequirements[string] buildRequirements; 208 BuildOptions[string] buildOptions; 209 210 211 /// Constructs a BuildSettings object from this template. 212 void getPlatformSettings(ref BuildSettings dst, in BuildPlatform platform, NativePath base_path) 213 const { 214 dst.targetType = this.targetType; 215 if (!this.targetPath.empty) dst.targetPath = this.targetPath; 216 if (!this.targetName.empty) dst.targetName = this.targetName; 217 if (!this.workingDirectory.empty) dst.workingDirectory = this.workingDirectory; 218 if (!this.mainSourceFile.empty) { 219 auto p = NativePath(this.mainSourceFile); 220 p.normalize(); 221 dst.mainSourceFile = p.toNativeString(); 222 dst.addSourceFiles(dst.mainSourceFile); 223 } 224 225 string[] collectFiles(in string[][string] paths_map, string pattern) 226 { 227 auto files = appender!(string[]); 228 229 import dub.project : buildSettingsVars; 230 import std.typecons : Nullable; 231 232 static Nullable!(string[string]) envVarCache; 233 234 if (envVarCache.isNull) envVarCache = environment.toAA(); 235 236 foreach (suffix, paths; paths_map) { 237 if (!platform.matchesSpecification(suffix)) 238 continue; 239 240 foreach (spath; paths) { 241 enforce(!spath.empty, "Paths must not be empty strings."); 242 auto path = NativePath(spath); 243 if (!path.absolute) path = base_path ~ path; 244 if (!existsFile(path) || !isDir(path.toNativeString())) { 245 import std.algorithm : any, find; 246 const hasVar = chain(buildSettingsVars, envVarCache.get.byKey).any!((string var) { 247 return spath.find("$"~var).length > 0 || spath.find("${"~var~"}").length > 0; 248 }); 249 if (!hasVar) 250 logWarn("Invalid source/import path: %s", path.toNativeString()); 251 continue; 252 } 253 254 auto pstr = path.toNativeString(); 255 foreach (d; dirEntries(pstr, pattern, SpanMode.depth)) { 256 import std.path : baseName, pathSplitter; 257 import std.algorithm.searching : canFind; 258 // eliminate any hidden files, or files in hidden directories. But always include 259 // files that are listed inside hidden directories that are specifically added to 260 // the project. 261 if (d.isDir || pathSplitter(d.name[pstr.length .. $]) 262 .canFind!(name => name.length && name[0] == '.')) 263 continue; 264 auto src = NativePath(d.name).relativeTo(base_path); 265 files ~= src.toNativeString(); 266 } 267 } 268 } 269 270 return files.data; 271 } 272 273 // collect source files 274 dst.addSourceFiles(collectFiles(sourcePaths, "*.d")); 275 auto sourceFiles = dst.sourceFiles.sort(); 276 277 // collect import files and remove sources 278 import std.algorithm : copy, setDifference; 279 280 auto importFiles = collectFiles(importPaths, "*.{d,di}").sort(); 281 immutable nremoved = importFiles.setDifference(sourceFiles).copy(importFiles.release).length; 282 importFiles = importFiles[0 .. $ - nremoved]; 283 dst.addImportFiles(importFiles.release); 284 285 dst.addStringImportFiles(collectFiles(stringImportPaths, "*")); 286 287 getPlatformSetting!("dflags", "addDFlags")(dst, platform); 288 getPlatformSetting!("lflags", "addLFlags")(dst, platform); 289 getPlatformSetting!("libs", "addLibs")(dst, platform); 290 getPlatformSetting!("sourceFiles", "addSourceFiles")(dst, platform); 291 getPlatformSetting!("excludedSourceFiles", "removeSourceFiles")(dst, platform); 292 getPlatformSetting!("copyFiles", "addCopyFiles")(dst, platform); 293 getPlatformSetting!("extraDependencyFiles", "addExtraDependencyFiles")(dst, platform); 294 getPlatformSetting!("versions", "addVersions")(dst, platform); 295 getPlatformSetting!("debugVersions", "addDebugVersions")(dst, platform); 296 getPlatformSetting!("versionFilters", "addVersionFilters")(dst, platform); 297 getPlatformSetting!("debugVersionFilters", "addDebugVersionFilters")(dst, platform); 298 getPlatformSetting!("importPaths", "addImportPaths")(dst, platform); 299 getPlatformSetting!("stringImportPaths", "addStringImportPaths")(dst, platform); 300 getPlatformSetting!("preGenerateCommands", "addPreGenerateCommands")(dst, platform); 301 getPlatformSetting!("postGenerateCommands", "addPostGenerateCommands")(dst, platform); 302 getPlatformSetting!("preBuildCommands", "addPreBuildCommands")(dst, platform); 303 getPlatformSetting!("postBuildCommands", "addPostBuildCommands")(dst, platform); 304 getPlatformSetting!("preRunCommands", "addPreRunCommands")(dst, platform); 305 getPlatformSetting!("postRunCommands", "addPostRunCommands")(dst, platform); 306 getPlatformSetting!("buildRequirements", "addRequirements")(dst, platform); 307 getPlatformSetting!("buildOptions", "addOptions")(dst, platform); 308 } 309 310 void getPlatformSetting(string name, string addname)(ref BuildSettings dst, in BuildPlatform platform) 311 const { 312 foreach(suffix, values; __traits(getMember, this, name)){ 313 if( platform.matchesSpecification(suffix) ) 314 __traits(getMember, dst, addname)(values); 315 } 316 } 317 318 void warnOnSpecialCompilerFlags(string package_name, string config_name) 319 { 320 auto nodef = false; 321 auto noprop = false; 322 foreach (req; this.buildRequirements) { 323 if (req & BuildRequirement.noDefaultFlags) nodef = true; 324 if (req & BuildRequirement.relaxProperties) noprop = true; 325 } 326 327 if (noprop) { 328 logWarn(`Warning: "buildRequirements": ["relaxProperties"] is deprecated and is now the default behavior. Note that the -property switch will probably be removed in future versions of DMD.`); 329 logWarn(""); 330 } 331 332 if (nodef) { 333 logWarn("Warning: This package uses the \"noDefaultFlags\" build requirement. Please use only for development purposes and not for released packages."); 334 logWarn(""); 335 } else { 336 string[] all_dflags; 337 BuildOptions all_options; 338 foreach (flags; this.dflags) all_dflags ~= flags; 339 foreach (options; this.buildOptions) all_options |= options; 340 .warnOnSpecialCompilerFlags(all_dflags, all_options, package_name, config_name); 341 } 342 } 343 } 344 345 package(dub) void checkPlatform(const scope ref ToolchainRequirements tr, BuildPlatform platform, string package_name) 346 { 347 import dub.compilers.utils : dmdLikeVersionToSemverLike; 348 import std.algorithm.iteration : map; 349 import std.format : format; 350 351 string compilerver; 352 Dependency compilerspec; 353 354 switch (platform.compiler) { 355 default: 356 compilerspec = Dependency.any; 357 compilerver = "0.0.0"; 358 break; 359 case "dmd": 360 compilerspec = tr.dmd; 361 compilerver = platform.compilerVersion.length 362 ? dmdLikeVersionToSemverLike(platform.compilerVersion) 363 : "0.0.0"; 364 break; 365 case "ldc": 366 compilerspec = tr.ldc; 367 compilerver = platform.compilerVersion; 368 if (!compilerver.length) compilerver = "0.0.0"; 369 break; 370 case "gdc": 371 compilerspec = tr.gdc; 372 compilerver = platform.compilerVersion; 373 if (!compilerver.length) compilerver = "0.0.0"; 374 break; 375 } 376 377 enforce(compilerspec != Dependency.invalid, 378 format( 379 "Installed %s %s is not supported by %s. Supported compiler(s):\n%s", 380 platform.compiler, platform.compilerVersion, package_name, 381 tr.supportedCompilers.map!((cs) { 382 auto str = " - " ~ cs[0]; 383 if (cs[1] != Dependency.any) str ~= ": " ~ cs[1].toString(); 384 return str; 385 }).join("\n") 386 ) 387 ); 388 389 enforce(compilerspec.matches(compilerver), 390 format( 391 "Installed %s-%s does not comply with %s compiler requirement: %s %s\n" ~ 392 "Please consider upgrading your installation.", 393 platform.compiler, platform.compilerVersion, 394 package_name, platform.compiler, compilerspec 395 ) 396 ); 397 398 enforce(tr.frontend.matches(dmdLikeVersionToSemverLike(platform.frontendVersionString)), 399 format( 400 "Installed %s-%s with frontend %s does not comply with %s frontend requirement: %s\n" ~ 401 "Please consider upgrading your installation.", 402 platform.compiler, platform.compilerVersion, 403 platform.frontendVersionString, package_name, tr.frontend 404 ) 405 ); 406 } 407 408 package bool addRequirement(ref ToolchainRequirements req, string name, string value) 409 { 410 switch (name) { 411 default: return false; 412 case "dub": req.dub = parseDependency(value); break; 413 case "frontend": req.frontend = parseDMDDependency(value); break; 414 case "ldc": req.ldc = parseDependency(value); break; 415 case "gdc": req.gdc = parseDependency(value); break; 416 case "dmd": req.dmd = parseDMDDependency(value); break; 417 } 418 return true; 419 } 420 421 private static Dependency parseDependency(string dep) 422 { 423 if (dep == "no") return Dependency.invalid; 424 return Dependency(dep); 425 } 426 427 private static Dependency parseDMDDependency(string dep) 428 { 429 import dub.compilers.utils : dmdLikeVersionToSemverLike; 430 import dub.dependency : Dependency; 431 import std.algorithm : map, splitter; 432 import std.array : join; 433 434 if (dep == "no") return Dependency.invalid; 435 return dep 436 .splitter(' ') 437 .map!(r => dmdLikeVersionToSemverLike(r)) 438 .join(' ') 439 .Dependency; 440 } 441 442 private T clone(T)(ref const(T) val) 443 { 444 import std.traits : isSomeString, isDynamicArray, isAssociativeArray, isBasicType, ValueType; 445 446 static if (is(T == immutable)) return val; 447 else static if (isBasicType!T) return val; 448 else static if (isDynamicArray!T) { 449 alias V = typeof(T.init[0]); 450 static if (is(V == immutable)) return val; 451 else { 452 T ret = new V[val.length]; 453 foreach (i, ref f; val) 454 ret[i] = clone!V(f); 455 return ret; 456 } 457 } else static if (isAssociativeArray!T) { 458 alias V = ValueType!T; 459 T ret; 460 foreach (k, ref f; val) 461 ret[k] = clone!V(f); 462 return ret; 463 } else static if (is(T == struct)) { 464 T ret; 465 foreach (i, M; typeof(T.tupleof)) 466 ret.tupleof[i] = clone!M(val.tupleof[i]); 467 return ret; 468 } else static assert(false, "Unsupported type: "~T.stringof); 469 } 470 471 unittest { // issue #1407 - duplicate main source file 472 { 473 BuildSettingsTemplate t; 474 t.mainSourceFile = "./foo.d"; 475 t.sourceFiles[""] = ["foo.d"]; 476 BuildSettings bs; 477 t.getPlatformSettings(bs, BuildPlatform.init, NativePath("/")); 478 assert(bs.sourceFiles == ["foo.d"]); 479 } 480 481 version (Windows) {{ 482 BuildSettingsTemplate t; 483 t.mainSourceFile = "src/foo.d"; 484 t.sourceFiles[""] = ["src\\foo.d"]; 485 BuildSettings bs; 486 t.getPlatformSettings(bs, BuildPlatform.init, NativePath("/")); 487 assert(bs.sourceFiles == ["src\\foo.d"]); 488 }} 489 }