1 /** Software package builder. 2 * 3 * Run as: dmd -debug -g -I.. -i xy.d -version=main && ./xy dmd druntime phobos dub DCD DustMite 4 * 5 * TODO: Support `xy REPO_URL`. Try for instance git@github.com:rui314/mold.git 6 * or https://github.com/rui314/mold.git. Supports matching repo with registered 7 * repos where repo name is stripped ending slash before matching. 8 * 9 * TODO: resolution-pyramid sample chronologically joined commits over dmd, druntime, phobos 10 * TODO: Experiment with indentation of stdout and stderr lines 11 * TODO: Show memory usage using https://code.dlang.org/packages/resusage 12 * TODO: Add install using installPrefix 13 * TODO: Skip building if fingerprint hasn’t changed unless --force -f is used. Put HEAD git-fingerprint directly under installPrefix 14 * TODO: Delete installPrefix prior to installation 15 * TODO: Add argparse --build=debug|release 16 * TODO: Pick compiler dmd if --build=debug 17 * TODO: Pick compiler ldmd2 if --build=release 18 * TODO: --test 19 */ 20 module nxt.xy; 21 22 import std.path : absolutePath, expandTilde, buildPath; 23 import std.stdio : writeln; 24 import std.file : chdir, exists, mkdir, FileException; 25 import std.process : Config, spawnProcess, wait, Pid; 26 import std.algorithm : canFind, joiner; 27 import std.conv : to; 28 import std.exception : enforce; 29 import std.getopt : getopt; 30 31 import nxt.fs; 32 import nxt.cmd; 33 34 @safe: 35 36 /// Spec Name. 37 struct Name { string value; alias value this; } 38 39 /// D `version` symbol. 40 alias DlangVersionName = string; 41 42 struct RemoteSpec 43 { 44 URL url; 45 string remoteName; 46 string[] repoBranches; 47 } 48 49 version(main) 50 int main(string[] args) 51 { 52 return buildN(args); 53 } 54 55 /** Build the packages `packages`. 56 */ 57 int buildN(scope const string[] packages) 58 { 59 const bool echo = true; // TODO: getopt flag being negation of make-like flag -s, --silent, --quiet 60 const BuildOption[] bos; 61 auto builds = makeSpecs(echo); 62 foreach (const name; packages) 63 if (Spec* buildPtr = Name(name) in builds) 64 (*buildPtr).go(bos, echo); 65 return 0; // TODO: propagate exit code 66 } 67 68 const DlangVersionName[] versionNames = ["cli"]; 69 const string[] dflagsLDCRelease = ["-m64", "-O3", "-release", "-boundscheck=off", "-enable-inlining", "-flto=full"]; 70 71 enum BuildOption 72 { 73 debugMode, /// Compile in debug mode (enables contracts, -debug) 74 releaseMode, /// Compile in release mode (disables assertions and bounds checks, -release) 75 ltoMode, /// Compile using LTO 76 pgoMode, /// Compile using PGO 77 coverage, /// Enable code coverage analysis (-cov) 78 debugInfo, /// Enable symbolic debug information (-g) 79 debugInfoC, /// Enable symbolic debug information in C compatible form (-gc) 80 alwaysStackFrame, /// Always generate a stack frame (-gs) 81 stackStomping, /// Perform stack stomping (-gx) 82 inline, /// Perform function inlining (-inline) 83 noBoundsCheck, /// Disable all bounds checking (-noboundscheck) 84 optimize, /// Enable optimizations (-O) 85 profile, /// Emit profiling code (-profile) 86 unittests, /// Compile unit tests (-unittest) 87 verbose, /// Verbose compiler output (-v) 88 ignoreUnknownPragmas, /// Ignores unknown pragmas during compilation (-ignore) 89 syntaxOnly, /// Don't generate object files (-o-) 90 warnings, /// Enable warnings (-wi) 91 warningsAsErrors, /// Treat warnings as errors (-w) 92 ignoreDeprecations, /// Do not warn about using deprecated features (-d) 93 deprecationWarnings, /// Warn about using deprecated features (-dw) 94 deprecationErrors, /// Stop compilation upon usage of deprecated features (-de) 95 property, /// DEPRECATED: Enforce property syntax (-property) 96 profileGC, /// Profile runtime allocations 97 pic, /// Generate position independent code 98 betterC, /// Compile in betterC mode (-betterC) 99 lowmem, /// Compile in lowmem mode (-lowmem) 100 } 101 102 Spec[Name] makeSpecs(bool echo) 103 { 104 typeof(return) specs; 105 { 106 const name = "vox"; 107 Spec spec = 108 { homePage : URL("https://github.com/MrSmith33/"~name~"/"), 109 name : Name(name), 110 repo : URL("https://github.com/MrSmith33/"~name~".git"), 111 compiler : ExePath("ldc2"), 112 versionNames : ["cli"], 113 sourceFilePaths : [Path("main.d")], 114 outFilePath : Path(name~".out"), 115 }; 116 specs[spec.name] = spec; 117 } 118 { 119 const name = "dmd"; 120 Spec spec = 121 { homePage : URL("http://dlang.org/"), 122 name : Name(name), 123 repo : URL("https://github.com/dlang/"~name~".git"), 124 // nordlow/selective-unittest-2 125 // nordlow/check-self-assign 126 // nordlow/check-unused 127 extraRemoteSpecs : [RemoteSpec(URL("https://github.com/nordlow/dmd.git"), "nordlow", 128 ["relax-alias-assign", 129 "traits-isarray-take2", 130 "diagnose-padding", 131 "aliasseq-bench", 132 "add-traits-hasAliasing", 133 "unique-qualifier"]), 134 // RemoteSpec(URL("https://github.com/UplinkCoder/dmd.git"), "skoch", ["newCTFE_upstream"]), 135 ], 136 // patches : [Patch(Path("new-ctfe.patch"), 1, echo)], 137 buildFlagsDefault : ["PIC=1"], 138 buildFlagsByBO : [BuildOption.debugMode : ["ENABLE_DEBUG=1"], 139 BuildOption.releaseMode : ["ENABLE_RELASE=1"]], 140 }; 141 specs[spec.name] = spec; 142 } 143 { 144 const name = "druntime"; 145 Spec spec = 146 { homePage : URL("http://dlang.org/"), 147 name : Name(name), 148 repo : URL("https://github.com/dlang/"~name~".git"), 149 }; 150 specs[spec.name] = spec; 151 } 152 { 153 const name = "phobos"; 154 Spec spec = 155 { homePage : URL("http://dlang.org/"), 156 name : Name(name), 157 repo : URL("https://github.com/dlang/"~name~".git"), 158 extraRemoteSpecs : [RemoteSpec(URL("https://github.com/nordlow/phobos.git"), "nordlow", ["faster-formatValue"])], 159 }; 160 specs[spec.name] = spec; 161 } 162 { 163 const name = "dub"; 164 Spec spec = 165 { homePage : URL("http://dlang.org/"), 166 name : Name(name), 167 repo : URL("https://github.com/dlang/"~name~".git"), 168 }; 169 specs[spec.name] = spec; 170 } 171 { 172 const name = "DustMite"; 173 Spec spec = 174 { homePage : URL("https://github.com/CyberShadow/"~name~"/wiki"), 175 name : Name(name), 176 compiler : ExePath("ldmd2"), 177 sources : ["dustmite.d", "polyhash.d", "splitter.d"], 178 repo : URL("https://github.com/CyberShadow/"~name~".git"), 179 }; 180 specs[spec.name] = spec; 181 } 182 { 183 const name = "DCD"; 184 Spec spec = 185 { name : Name(name), 186 repo : URL("https://github.com/dlang-community/"~name~".git"), 187 buildFlagsDefault : ["ldc"], 188 buildFlagsByBO : [BuildOption.debugMode : ["debug"], 189 BuildOption.releaseMode : ["ldc"]], 190 }; 191 specs[spec.name] = spec; 192 } 193 { 194 const name = "mold"; 195 Spec spec = 196 { name : Name(name), 197 repo : URL("https://github.com/rui314/"~name~".git"), 198 }; 199 specs[spec.name] = spec; 200 } 201 return specs; 202 } 203 204 /** Launch/Execution specification. 205 */ 206 @safe struct Spec 207 { 208 URL homePage; 209 Name name; 210 211 // Git sources 212 URL repo; 213 string remote = "origin"; 214 string branch; 215 RemoteSpec[] extraRemoteSpecs; 216 Patch[] patches; 217 218 ExePath compiler; 219 string[] sources; 220 string[] buildFlagsDefault; 221 string[][BuildOption] buildFlagsByBO; 222 DlangVersionName[] versionNames; 223 Path[] sourceFilePaths; 224 Path outFilePath; 225 uint jobCount; 226 bool recurseSubModulesFlag = true; 227 228 void go(scope ref const BuildOption[] bos, in bool echo) scope 229 { 230 const dlDirName = DirName(".cache/archives"); 231 const dlDir = DirPath(("~/" ~ dlDirName ~ "/").expandTilde.buildPath(name)); 232 writeln(); 233 fetch(bos, dlDir, echo); 234 build(bos, dlDir, echo); 235 } 236 237 void fetch(scope ref const BuildOption[] bos, in DirPath dlDir, in bool echo) scope 238 { 239 import core.thread : Thread; 240 import core.time : dur; 241 auto repo = Repository(repo, dlDir, echo); 242 // TODO: use waitAllInSequence(); 243 enforce(!repo.cloneOrRefresh(recurseSubModulesFlag, remote, branch).wait()); 244 enforce(!repo.clean().wait()); 245 string[] remoteNames; 246 foreach (const spec; extraRemoteSpecs) 247 { 248 remoteNames ~= spec.remoteName; 249 const statusIgnored = repo.remoteRemove(spec.remoteName).wait(); // ok if already removed 250 enforce(!repo.remoteAdd(spec.url, spec.remoteName).wait()); 251 } 252 enforce(!repo.fetch(remoteNames).wait()); 253 foreach (const spec; extraRemoteSpecs) 254 foreach (const branch; spec.repoBranches) 255 enforce(!repo.merge([spec.remoteName~"/"~branch], ["--no-edit", "-Xignore-all-space"]).wait()); 256 foreach (ref patch; patches) 257 enforce(!patch.applyIn(dlDir).wait()); 258 } 259 260 string[] bosFlags(scope ref const BuildOption[] bos) const scope pure nothrow 261 { 262 typeof(return) result; 263 foreach (bo; bos) 264 result ~= buildFlagsByBO[bo]; 265 return result; 266 } 267 268 string[] allBuildFlags(scope ref const BuildOption[] bos) const scope pure nothrow 269 { 270 return buildFlagsDefault ~ bosFlags(bos) ~ jobFlags() ~ sources; 271 } 272 273 void build(scope ref const BuildOption[] bos, in DirPath workDir, in bool echo) const scope 274 { 275 string[] args; 276 Config config = Config.none; 277 size_t maxOutput = size_t.max; 278 279 chdir(workDir); // TODO: replace with absolute paths in exists or dir.exists 280 281 // TODO: when serveral of this exists ask user 282 if (exists("Makefile")) 283 args = ["make", "-f", "Makefile"] ~ allBuildFlags(bos); 284 else if (exists("makefile")) 285 args = ["make", "-f", "makefile"] ~ allBuildFlags(bos); 286 else if (exists("posix.mak")) 287 args = ["make", "-f", "posix.mak"] ~ allBuildFlags(bos); 288 else if (exists("dub.sdl") || 289 exists("dub.json")) 290 args = ["dub", "build"]; 291 else 292 { 293 args = [compiler]; 294 if (bos.canFind(BuildOption.releaseMode) && 295 (compiler[].canFind("ldc2") || 296 compiler[].canFind("ldmd2"))) 297 args ~= dflagsLDCRelease; // TODO: make declarative 298 foreach (const ver; versionNames) 299 args ~= ("-d-version=" ~ ver); 300 args ~= sources; 301 } 302 /* ${DC} -release -boundscheck=off dustmite.d splitter.d polyhash.d */ 303 /* mkdir -p ${DMD_EXEC_PREFIX} */ 304 /* mv dustmite ${DMD_EXEC_PREFIX} */ 305 306 enforce(args.length, "Could not deduce build command"); 307 308 // TOOD: wrap in Spawn 309 writeln("Building as `", args.joiner(" "), "` ..."); 310 string[string] env_; 311 /* TODO: add a wrapper spawnProcess1 called by Patch.applyIn, 312 * Repository.spawn and here that sets stdout, stderr based on bool 313 * echo. */ 314 scope Pid pid = spawnProcess(args, env_, config, workDir); 315 const status = pid.wait(); 316 if (status != 0) 317 writeln("Compilation failed:\n"); 318 if (status != 0) 319 writeln("Compilation successful:\n"); 320 } 321 322 string[] jobFlags() const scope pure nothrow 323 { 324 return jobCount ? ["-j"~jobCount.to!string] : []; 325 } 326 } 327 328 /// Relaxed variant of `mkdir`. 329 void mkdirRelaxed(in const(char)[] pathname) 330 { 331 try 332 mkdir(pathname); 333 catch (FileException e) {} // TODO: avoid need for throwing 334 } 335 336 int[] waitAllInSequence(scope Pid[] pids...) 337 { 338 typeof(return) statuses; 339 foreach (pid; pids) 340 statuses ~= pid.wait(); 341 return statuses; 342 } 343 344 int[] waitAllInParallel(scope Pid[] pids...) 345 { 346 assert(0, "TODO: implement"); 347 }