Newer
Older
dub_jkp / source / dub / package_.d
  1. /**
  2. Stuff with 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
  7. */
  8. module dub.package_;
  9.  
  10. public import dub.recipe.packagerecipe;
  11.  
  12. import dub.compilers.compiler;
  13. import dub.dependency;
  14. import dub.recipe.json;
  15. import dub.recipe.sdl;
  16.  
  17. import dub.internal.utils;
  18. import dub.internal.vibecompat.core.log;
  19. import dub.internal.vibecompat.core.file;
  20. import dub.internal.vibecompat.data.json;
  21. import dub.internal.vibecompat.inet.url;
  22.  
  23. import std.algorithm;
  24. import std.array;
  25. import std.conv;
  26. import std.exception;
  27. import std.file;
  28. import std.range;
  29. import std.string;
  30. import std.traits : EnumMembers;
  31.  
  32.  
  33.  
  34. enum PackageFormat { json, sdl }
  35. struct FilenameAndFormat
  36. {
  37. string filename;
  38. PackageFormat format;
  39. }
  40. struct PathAndFormat
  41. {
  42. Path path;
  43. PackageFormat format;
  44. @property bool empty() { return path.empty; }
  45. string toString() { return path.toString(); }
  46. }
  47.  
  48. // Supported package descriptions in decreasing order of preference.
  49. enum FilenameAndFormat[] packageInfoFiles = [
  50. {"dub.json", PackageFormat.json},
  51. /*{"dub.sdl",PackageFormat.sdl},*/
  52. {"package.json", PackageFormat.json}
  53. ];
  54.  
  55. string defaultPackageFilename() {
  56. return packageInfoFiles[0].filename;
  57. }
  58.  
  59. /**
  60. Represents a package, including its sub packages
  61.  
  62. Documentation of the dub.json can be found at
  63. http://registry.vibed.org/package-format
  64. */
  65. class Package {
  66. static struct LocalPackageDef { string name; Version version_; Path path; }
  67.  
  68. private {
  69. Path m_path;
  70. PathAndFormat m_infoFile;
  71. PackageRecipe m_info;
  72. Package m_parentPackage;
  73. }
  74.  
  75. static PathAndFormat findPackageFile(Path path)
  76. {
  77. foreach(file; packageInfoFiles) {
  78. auto filename = path ~ file.filename;
  79. if(existsFile(filename)) return PathAndFormat(filename, file.format);
  80. }
  81. return PathAndFormat(Path());
  82. }
  83.  
  84. this(Path root, PathAndFormat infoFile = PathAndFormat(), Package parent = null, string versionOverride = "")
  85. {
  86. RawPackage raw_package;
  87. m_infoFile = infoFile;
  88.  
  89. try {
  90. if(m_infoFile.empty) {
  91. m_infoFile = findPackageFile(root);
  92. if(m_infoFile.empty) throw new Exception("no package file was found, expected one of the following: "~to!string(packageInfoFiles));
  93. }
  94. raw_package = rawPackageFromFile(m_infoFile);
  95. } catch (Exception ex) throw ex;//throw new Exception(format("Failed to load package %s: %s", m_infoFile.toNativeString(), ex.msg));
  96.  
  97. enforce(raw_package !is null, format("Missing package description for package at %s", root.toNativeString()));
  98. this(raw_package, root, parent, versionOverride);
  99. }
  100.  
  101. this(Json package_info, Path root = Path(), Package parent = null, string versionOverride = "")
  102. {
  103. this(new JsonPackage(package_info), root, parent, versionOverride);
  104. }
  105.  
  106. this(const RawPackage raw_package, Path root = Path(), Package parent = null, string versionOverride = "")
  107. {
  108. PackageRecipe recipe;
  109.  
  110. // parse the Package description
  111. if(raw_package !is null)
  112. {
  113. scope(failure) logError("Failed to parse package description in %s", root.toNativeString());
  114. raw_package.parseInto(recipe, parent ? parent.name : null);
  115.  
  116. if (!versionOverride.empty)
  117. recipe.version_ = versionOverride;
  118.  
  119. // try to run git to determine the version of the package if no explicit version was given
  120. if (recipe.version_.length == 0 && !parent) {
  121. try recipe.version_ = determineVersionFromSCM(root);
  122. catch (Exception e) logDebug("Failed to determine version by SCM: %s", e.msg);
  123.  
  124. if (recipe.version_.length == 0) {
  125. logDiagnostic("Note: Failed to determine version of package %s at %s. Assuming ~master.", recipe.name, this.path.toNativeString());
  126. // TODO: Assume unknown version here?
  127. // recipe.version_ = Version.UNKNOWN.toString();
  128. recipe.version_ = Version.MASTER.toString();
  129. } else logDiagnostic("Determined package version using GIT: %s %s", recipe.name, recipe.version_);
  130. }
  131. }
  132.  
  133. this(recipe, root, parent);
  134. }
  135.  
  136. this(PackageRecipe recipe, Path root = Path(), Package parent = null)
  137. {
  138. m_parentPackage = parent;
  139. m_path = root;
  140. m_path.endsWithSlash = true;
  141.  
  142. // check for default string import folders
  143. foreach(defvf; ["views"]){
  144. auto p = m_path ~ defvf;
  145. if( existsFile(p) )
  146. m_info.buildSettings.stringImportPaths[""] ~= defvf;
  147. }
  148.  
  149. // use the given recipe as the basis
  150. m_info = recipe;
  151.  
  152. // WARNING: changed semantics here. Previously, "sourcePaths" etc.
  153. // could overwrite what was determined here. Now the default paths
  154. // are always added. This must be fixed somehow!
  155.  
  156. // check for default source folders
  157. string app_main_file;
  158. auto pkg_name = recipe.name.length ? recipe.name : "unknown";
  159. foreach(defsf; ["source/", "src/"]){
  160. auto p = m_path ~ defsf;
  161. if( existsFile(p) ){
  162. m_info.buildSettings.sourcePaths[""] ~= defsf;
  163. m_info.buildSettings.importPaths[""] ~= defsf;
  164. foreach (fil; ["app.d", "main.d", pkg_name ~ "/main.d", pkg_name ~ "/" ~ "app.d"])
  165. if (existsFile(p ~ fil)) {
  166. app_main_file = Path(defsf ~ fil).toNativeString();
  167. break;
  168. }
  169. }
  170. }
  171.  
  172. // generate default configurations if none are defined
  173. if (m_info.configurations.length == 0) {
  174. if (m_info.buildSettings.targetType == TargetType.executable) {
  175. BuildSettingsTemplate app_settings;
  176. app_settings.targetType = TargetType.executable;
  177. if (m_info.buildSettings.mainSourceFile.empty) app_settings.mainSourceFile = app_main_file;
  178. m_info.configurations ~= ConfigurationInfo("application", app_settings);
  179. } else if (m_info.buildSettings.targetType != TargetType.none) {
  180. BuildSettingsTemplate lib_settings;
  181. lib_settings.targetType = m_info.buildSettings.targetType == TargetType.autodetect ? TargetType.library : m_info.buildSettings.targetType;
  182.  
  183. if (m_info.buildSettings.targetType == TargetType.autodetect) {
  184. if (app_main_file.length) {
  185. lib_settings.excludedSourceFiles[""] ~= app_main_file;
  186.  
  187. BuildSettingsTemplate app_settings;
  188. app_settings.targetType = TargetType.executable;
  189. app_settings.mainSourceFile = app_main_file;
  190. m_info.configurations ~= ConfigurationInfo("application", app_settings);
  191. }
  192. }
  193.  
  194. m_info.configurations ~= ConfigurationInfo("library", lib_settings);
  195. }
  196. }
  197. simpleLint();
  198. }
  199.  
  200. @property string name()
  201. const {
  202. if (m_parentPackage) return m_parentPackage.name ~ ":" ~ m_info.name;
  203. else return m_info.name;
  204. }
  205. @property string vers() const { return m_parentPackage ? m_parentPackage.vers : m_info.version_; }
  206. @property Version ver() const { return Version(this.vers); }
  207. @property void ver(Version ver) { assert(m_parentPackage is null); m_info.version_ = ver.toString(); }
  208. @property ref inout(PackageRecipe) info() inout { return m_info; }
  209. @property Path path() const { return m_path; }
  210. @property Path packageInfoFilename() const { return m_infoFile.path; }
  211. @property const(Dependency[string]) dependencies() const { return m_info.dependencies; }
  212. @property inout(Package) basePackage() inout { return m_parentPackage ? m_parentPackage.basePackage : this; }
  213. @property inout(Package) parentPackage() inout { return m_parentPackage; }
  214. @property inout(SubPackage)[] subPackages() inout { return m_info.subPackages; }
  215.  
  216. @property string[] configurations()
  217. const {
  218. auto ret = appender!(string[])();
  219. foreach( ref config; m_info.configurations )
  220. ret.put(config.name);
  221. return ret.data;
  222. }
  223.  
  224. const(Dependency[string]) getDependencies(string config)
  225. const {
  226. Dependency[string] ret;
  227. foreach (k, v; m_info.buildSettings.dependencies)
  228. ret[k] = v;
  229. foreach (ref conf; m_info.configurations)
  230. if (conf.name == config) {
  231. foreach (k, v; conf.buildSettings.dependencies)
  232. ret[k] = v;
  233. break;
  234. }
  235. return ret;
  236. }
  237.  
  238. /** Overwrites the packge description file using the default filename with the current information.
  239. */
  240. void storeInfo()
  241. {
  242. enforce(!ver.isUnknown, "Trying to store a package with an 'unknown' version, this is not supported.");
  243. auto filename = m_path ~ defaultPackageFilename();
  244. auto dstFile = openFile(filename.toNativeString(), FileMode.CreateTrunc);
  245. scope(exit) dstFile.close();
  246. dstFile.writePrettyJsonString(m_info.toJson());
  247. m_infoFile = PathAndFormat(filename);
  248. }
  249.  
  250. /*inout(Package) getSubPackage(string name, bool silent_fail = false)
  251. inout {
  252. foreach (p; m_info.subPackages)
  253. if (p.package_ !is null && p.package_.name == this.name ~ ":" ~ name)
  254. return p.package_;
  255. enforce(silent_fail, format("Unknown sub package: %s:%s", this.name, name));
  256. return null;
  257. }*/
  258.  
  259. void warnOnSpecialCompilerFlags()
  260. {
  261. // warn about use of special flags
  262. m_info.buildSettings.warnOnSpecialCompilerFlags(m_info.name, null);
  263. foreach (ref config; m_info.configurations)
  264. config.buildSettings.warnOnSpecialCompilerFlags(m_info.name, config.name);
  265. }
  266.  
  267. const(BuildSettingsTemplate) getBuildSettings(string config = null)
  268. const {
  269. if (config.length) {
  270. foreach (ref conf; m_info.configurations)
  271. if (conf.name == config)
  272. return conf.buildSettings;
  273. assert(false, "Unknown configuration: "~config);
  274. } else {
  275. return m_info.buildSettings;
  276. }
  277. }
  278.  
  279. /// Returns all BuildSettings for the given platform and config.
  280. BuildSettings getBuildSettings(in BuildPlatform platform, string config)
  281. const {
  282. BuildSettings ret;
  283. m_info.buildSettings.getPlatformSettings(ret, platform, this.path);
  284. bool found = false;
  285. foreach(ref conf; m_info.configurations){
  286. if( conf.name != config ) continue;
  287. conf.buildSettings.getPlatformSettings(ret, platform, this.path);
  288. found = true;
  289. break;
  290. }
  291. assert(found || config is null, "Unknown configuration for "~m_info.name~": "~config);
  292.  
  293. // construct default target name based on package name
  294. if( ret.targetName.empty ) ret.targetName = this.name.replace(":", "_");
  295.  
  296. // special support for DMD style flags
  297. getCompiler("dmd").extractBuildOptions(ret);
  298.  
  299. return ret;
  300. }
  301.  
  302. /// Returns the combination of all build settings for all configurations and platforms
  303. BuildSettings getCombinedBuildSettings()
  304. const {
  305. BuildSettings ret;
  306. m_info.buildSettings.getPlatformSettings(ret, BuildPlatform.any, this.path);
  307. foreach(ref conf; m_info.configurations)
  308. conf.buildSettings.getPlatformSettings(ret, BuildPlatform.any, this.path);
  309.  
  310. // construct default target name based on package name
  311. if (ret.targetName.empty) ret.targetName = this.name.replace(":", "_");
  312.  
  313. // special support for DMD style flags
  314. getCompiler("dmd").extractBuildOptions(ret);
  315.  
  316. return ret;
  317. }
  318.  
  319. void addBuildTypeSettings(ref BuildSettings settings, in BuildPlatform platform, string build_type)
  320. const {
  321. if (build_type == "$DFLAGS") {
  322. import std.process;
  323. string dflags = environment.get("DFLAGS");
  324. settings.addDFlags(dflags.split());
  325. return;
  326. }
  327.  
  328. if (auto pbt = build_type in m_info.buildTypes) {
  329. logDiagnostic("Using custom build type '%s'.", build_type);
  330. pbt.getPlatformSettings(settings, platform, this.path);
  331. } else {
  332. with(BuildOptions) switch (build_type) {
  333. default: throw new Exception(format("Unknown build type for %s: '%s'", this.name, build_type));
  334. case "plain": break;
  335. case "debug": settings.addOptions(debugMode, debugInfo); break;
  336. case "release": settings.addOptions(releaseMode, optimize, inline); break;
  337. case "release-nobounds": settings.addOptions(releaseMode, optimize, inline, noBoundsCheck); break;
  338. case "unittest": settings.addOptions(unittests, debugMode, debugInfo); break;
  339. case "docs": settings.addOptions(syntaxOnly); settings.addDFlags("-c", "-Dddocs"); break;
  340. case "ddox": settings.addOptions(syntaxOnly); settings.addDFlags("-c", "-Df__dummy.html", "-Xfdocs.json"); break;
  341. case "profile": settings.addOptions(profile, optimize, inline, debugInfo); break;
  342. case "cov": settings.addOptions(coverage, debugInfo); break;
  343. case "unittest-cov": settings.addOptions(unittests, coverage, debugMode, debugInfo); break;
  344. }
  345. }
  346. }
  347.  
  348. string getSubConfiguration(string config, in Package dependency, in BuildPlatform platform)
  349. const {
  350. bool found = false;
  351. foreach(ref c; m_info.configurations){
  352. if( c.name == config ){
  353. if( auto pv = dependency.name in c.buildSettings.subConfigurations ) return *pv;
  354. found = true;
  355. break;
  356. }
  357. }
  358. assert(found || config is null, "Invalid configuration \""~config~"\" for "~this.name);
  359. if( auto pv = dependency.name in m_info.buildSettings.subConfigurations ) return *pv;
  360. return null;
  361. }
  362.  
  363. /// Returns the default configuration to build for the given platform
  364. string getDefaultConfiguration(in BuildPlatform platform, bool allow_non_library = false)
  365. const {
  366. foreach (ref conf; m_info.configurations) {
  367. if (!conf.matchesPlatform(platform)) continue;
  368. if (!allow_non_library && conf.buildSettings.targetType == TargetType.executable) continue;
  369. return conf.name;
  370. }
  371. return null;
  372. }
  373.  
  374. /// Returns a list of configurations suitable for the given platform
  375. string[] getPlatformConfigurations(in BuildPlatform platform, bool is_main_package = false)
  376. const {
  377. auto ret = appender!(string[]);
  378. foreach(ref conf; m_info.configurations){
  379. if (!conf.matchesPlatform(platform)) continue;
  380. if (!is_main_package && conf.buildSettings.targetType == TargetType.executable) continue;
  381. ret ~= conf.name;
  382. }
  383. if (ret.data.length == 0) ret.put(null);
  384. return ret.data;
  385. }
  386.  
  387. /// Human readable information of this package and its dependencies.
  388. string generateInfoString() const {
  389. string s;
  390. s ~= m_info.name ~ ", version '" ~ m_info.version_ ~ "'";
  391. s ~= "\n Dependencies:";
  392. foreach(string p, ref const Dependency v; m_info.dependencies)
  393. s ~= "\n " ~ p ~ ", version '" ~ v.toString() ~ "'";
  394. return s;
  395. }
  396.  
  397. bool hasDependency(string depname, string config)
  398. const {
  399. if (depname in m_info.buildSettings.dependencies) return true;
  400. foreach (ref c; m_info.configurations)
  401. if ((config.empty || c.name == config) && depname in c.buildSettings.dependencies)
  402. return true;
  403. return false;
  404. }
  405.  
  406. void describe(ref Json dst, BuildPlatform platform, string config)
  407. {
  408. dst.path = m_path.toNativeString();
  409. dst.name = this.name;
  410. dst["version"] = this.vers;
  411. dst.description = m_info.description;
  412. dst.homepage = m_info.homepage;
  413. dst.authors = m_info.authors.serializeToJson();
  414. dst.copyright = m_info.copyright;
  415. dst.license = m_info.license;
  416. dst.dependencies = m_info.dependencies.keys.serializeToJson();
  417.  
  418. // save build settings
  419. BuildSettings bs = getBuildSettings(platform, config);
  420. BuildSettings allbs = getCombinedBuildSettings();
  421.  
  422. foreach (string k, v; bs.serializeToJson()) dst[k] = v;
  423. dst.remove("requirements");
  424. dst.remove("sourceFiles");
  425. dst.remove("importFiles");
  426. dst.remove("stringImportFiles");
  427. dst.targetType = bs.targetType.to!string();
  428. if (dst.targetType != TargetType.none)
  429. dst.targetFileName = getTargetFileName(bs, platform);
  430.  
  431. // prettify build requirements output
  432. Json[] breqs;
  433. for (int i = 1; i <= BuildRequirements.max; i <<= 1)
  434. if (bs.requirements & i)
  435. breqs ~= Json(to!string(cast(BuildRequirements)i));
  436. dst.buildRequirements = breqs;
  437.  
  438. // prettify options output
  439. Json[] bopts;
  440. for (int i = 1; i <= BuildOptions.max; i <<= 1)
  441. if (bs.options & i)
  442. bopts ~= Json(to!string(cast(BuildOptions)i));
  443. dst.options = bopts;
  444.  
  445. // collect all possible source files and determine their types
  446. string[string] sourceFileTypes;
  447. foreach (f; allbs.stringImportFiles) sourceFileTypes[f] = "unusedStringImport";
  448. foreach (f; allbs.importFiles) sourceFileTypes[f] = "unusedImport";
  449. foreach (f; allbs.sourceFiles) sourceFileTypes[f] = "unusedSource";
  450. foreach (f; bs.stringImportFiles) sourceFileTypes[f] = "stringImport";
  451. foreach (f; bs.importFiles) sourceFileTypes[f] = "import";
  452. foreach (f; bs.sourceFiles) sourceFileTypes[f] = "source";
  453. Json[] files;
  454. foreach (f; sourceFileTypes.byKey.array.sort) {
  455. auto jf = Json.emptyObject;
  456. jf["path"] = f;
  457. jf["type"] = sourceFileTypes[f];
  458. files ~= jf;
  459. }
  460. dst.files = Json(files);
  461. }
  462.  
  463. private void simpleLint() const {
  464. if (m_parentPackage) {
  465. if (m_parentPackage.path != path) {
  466. if (info.license.length && info.license != m_parentPackage.info.license)
  467. logWarn("License in subpackage %s is different than it's parent package, this is discouraged.", name);
  468. }
  469. }
  470. if (name.empty()) logWarn("The package in %s has no name.", path);
  471. }
  472.  
  473. private static RawPackage rawPackageFromFile(PathAndFormat file, bool silent_fail = false) {
  474. if( silent_fail && !existsFile(file.path) ) return null;
  475. auto f = openFile(file.path.toNativeString(), FileMode.Read);
  476. scope(exit) f.close();
  477. auto text = stripUTF8Bom(cast(string)f.readAll());
  478.  
  479. final switch(file.format) {
  480. case PackageFormat.json:
  481. return new JsonPackage(parseJsonString(text));
  482. case PackageFormat.sdl:
  483. if(silent_fail) return null; throw new Exception("SDL not implemented");
  484. }
  485. }
  486.  
  487. static abstract class RawPackage
  488. {
  489. string package_name; // Should already be lower case
  490. string version_;
  491. abstract void parseInto(ref PackageRecipe package_, string parent_name) const;
  492. }
  493. private static class JsonPackage : RawPackage
  494. {
  495. Json json;
  496. this(Json json) {
  497. this.json = json;
  498.  
  499. string nameLower;
  500. if(json.type == Json.Type.string) {
  501. nameLower = json.get!string.toLower();
  502. this.json = nameLower;
  503. } else {
  504. nameLower = json.name.get!string.toLower();
  505. this.json.name = nameLower;
  506. this.package_name = nameLower;
  507.  
  508. Json versionJson = json["version"];
  509. this.version_ = (versionJson.type == Json.Type.undefined) ? null : versionJson.get!string;
  510. }
  511.  
  512. this.package_name = nameLower;
  513. }
  514. override void parseInto(ref PackageRecipe recipe, string parent_name) const
  515. {
  516. recipe.parseJson(json, parent_name);
  517. }
  518. }
  519. private static class SdlPackage : RawPackage
  520. {
  521. override void parseInto(ref PackageRecipe package_, string parent_name) const
  522. {
  523. throw new Exception("SDL packages not implemented yet");
  524. }
  525. }
  526. }
  527.  
  528.  
  529. private string determineVersionFromSCM(Path path)
  530. {
  531. import std.process;
  532. import dub.semver;
  533.  
  534. auto git_dir = path ~ ".git";
  535. if (!existsFile(git_dir) || !isDir(git_dir.toNativeString)) return null;
  536. auto git_dir_param = "--git-dir=" ~ git_dir.toNativeString();
  537.  
  538. static string exec(scope string[] params...) {
  539. auto ret = executeShell(escapeShellCommand(params));
  540. if (ret.status == 0) return ret.output.strip;
  541. logDebug("'%s' failed with exit code %s: %s", params.join(" "), ret.status, ret.output.strip);
  542. return null;
  543. }
  544.  
  545. if (auto tag = exec("git", git_dir_param, "describe", "--long", "--tags")) {
  546. auto parts = tag.split("-");
  547. auto commit = parts[$-1];
  548. auto num = parts[$-2].to!int;
  549. tag = parts[0 .. $-2].join("-");
  550. if (tag.startsWith("v") && isValidVersion(tag[1 .. $])) {
  551. if (num == 0) return tag[1 .. $];
  552. else if (tag.canFind("+")) return format("%s.commit.%s.%s", tag[1 .. $], num, commit);
  553. else return format("%s+commit.%s.%s", tag[1 .. $], num, commit);
  554. }
  555. }
  556.  
  557. if (auto branch = exec("git", git_dir_param, "rev-parse", "--abbrev-ref", "HEAD")) {
  558. if (branch != "HEAD") return "~" ~ branch;
  559. }
  560.  
  561. return null;
  562. }