| 1 |
// Written in the D programming language. |
|---|
| 2 |
|
|---|
| 3 |
import std.algorithm, std.c.stdlib, std.contracts, std.date, |
|---|
| 4 |
std.file, std.getopt, |
|---|
| 5 |
std.md5, std.path, std.process, std.regexp, |
|---|
| 6 |
std.stdio, std.string, std.typetuple; |
|---|
| 7 |
|
|---|
| 8 |
version (Posix) |
|---|
| 9 |
{ |
|---|
| 10 |
enum objExt = ".o"; |
|---|
| 11 |
enum binExt = ""; |
|---|
| 12 |
} |
|---|
| 13 |
else version (Windows) |
|---|
| 14 |
{ |
|---|
| 15 |
enum objExt = ".obj"; |
|---|
| 16 |
enum binExt = ".exe"; |
|---|
| 17 |
} |
|---|
| 18 |
else |
|---|
| 19 |
{ |
|---|
| 20 |
static assert(0); |
|---|
| 21 |
} |
|---|
| 22 |
|
|---|
| 23 |
private bool chatty, buildOnly, dryRun, force; |
|---|
| 24 |
private string exe, compiler = "dmd"; |
|---|
| 25 |
|
|---|
| 26 |
int main(string[] args) |
|---|
| 27 |
{ |
|---|
| 28 |
//writeln("Invoked with: ", map!(q{a ~ ", "})(args)); |
|---|
| 29 |
if (args.length > 1 && std.algorithm.startsWith(args[1], |
|---|
| 30 |
"--shebang ", "--shebang=")) |
|---|
| 31 |
{ |
|---|
| 32 |
// multiple options wrapped in one |
|---|
| 33 |
auto a = args[1]["--shebang ".length .. $]; |
|---|
| 34 |
args = args[0 .. 1] ~ split(a) ~ args[2 .. $]; |
|---|
| 35 |
} |
|---|
| 36 |
|
|---|
| 37 |
// Continue parsing the command line; now get rdmd's own arguments |
|---|
| 38 |
// parse the -o option |
|---|
| 39 |
void dashOh(string key, string value) |
|---|
| 40 |
{ |
|---|
| 41 |
if (value[0] == 'f') |
|---|
| 42 |
{ |
|---|
| 43 |
// -ofmyfile passed |
|---|
| 44 |
exe = value[1 .. $]; |
|---|
| 45 |
} |
|---|
| 46 |
else if (value[0] == 'd') |
|---|
| 47 |
{ |
|---|
| 48 |
// -odmydir passed |
|---|
| 49 |
// add a trailing path separator to clarify it's a dir |
|---|
| 50 |
exe = std.path.join(value[1 .. $], ""); |
|---|
| 51 |
assert(std.algorithm.endsWith(exe, std.path.sep[])); |
|---|
| 52 |
} |
|---|
| 53 |
else if (value[0] == '-') |
|---|
| 54 |
{ |
|---|
| 55 |
// -o- passed |
|---|
| 56 |
enforce(false, "Option -o- currently not supported by rdmd"); |
|---|
| 57 |
} |
|---|
| 58 |
else |
|---|
| 59 |
{ |
|---|
| 60 |
enforce(false, "Unrecognized option: "~key~value); |
|---|
| 61 |
} |
|---|
| 62 |
} |
|---|
| 63 |
|
|---|
| 64 |
// start the web browser on documentation page |
|---|
| 65 |
void man() |
|---|
| 66 |
{ |
|---|
| 67 |
foreach (b; [ std.process.getenv("BROWSER"), "firefox", |
|---|
| 68 |
"sensible-browser", "x-www-browser" ]) { |
|---|
| 69 |
if (!b.length) continue; |
|---|
| 70 |
if (!system(b~" http://www.digitalmars.com/d/2.0/rdmd.html")) |
|---|
| 71 |
return; |
|---|
| 72 |
} |
|---|
| 73 |
} |
|---|
| 74 |
|
|---|
| 75 |
bool bailout; // bailout set by functions called in getopt if |
|---|
| 76 |
// program should exit |
|---|
| 77 |
string[] loop; // set by --loop |
|---|
| 78 |
bool addStubMain;// set by --main |
|---|
| 79 |
string[] eval; // set by --eval |
|---|
| 80 |
getopt(args, |
|---|
| 81 |
std.getopt.config.caseSensitive, |
|---|
| 82 |
std.getopt.config.passThrough, |
|---|
| 83 |
std.getopt.config.stopOnFirstNonOption, |
|---|
| 84 |
"build-only", &buildOnly, |
|---|
| 85 |
"chatty", &chatty, |
|---|
| 86 |
"dry-run", &dryRun, |
|---|
| 87 |
"force", &force, |
|---|
| 88 |
"help", (string) { writeln(helpString); bailout = true; }, |
|---|
| 89 |
"main", &addStubMain, |
|---|
| 90 |
"man", (string) { man; bailout = true; }, |
|---|
| 91 |
"eval", &eval, |
|---|
| 92 |
"loop", &loop, |
|---|
| 93 |
"o", &dashOh, |
|---|
| 94 |
"compiler", &compiler); |
|---|
| 95 |
if (bailout) return 0; |
|---|
| 96 |
if (dryRun) chatty = true; // dry-run implies chatty |
|---|
| 97 |
|
|---|
| 98 |
// Just evaluate this program! |
|---|
| 99 |
if (loop) |
|---|
| 100 |
{ |
|---|
| 101 |
return .eval(importWorld ~ "void main(char[][] args) { " |
|---|
| 102 |
~ "foreach (line; stdin.byLine()) {\n" ~ join(loop, "\n") |
|---|
| 103 |
~ ";\n} }"); |
|---|
| 104 |
} |
|---|
| 105 |
if (eval) |
|---|
| 106 |
{ |
|---|
| 107 |
return .eval(importWorld ~ "void main(char[][] args) {\n" |
|---|
| 108 |
~ join(eval, "\n") ~ ";\n}"); |
|---|
| 109 |
} |
|---|
| 110 |
|
|---|
| 111 |
// Parse the program line - first find the program to run |
|---|
| 112 |
uint programPos = 1; |
|---|
| 113 |
for (;; ++programPos) |
|---|
| 114 |
{ |
|---|
| 115 |
if (programPos == args.length) |
|---|
| 116 |
{ |
|---|
| 117 |
write(helpString); |
|---|
| 118 |
return 1; |
|---|
| 119 |
} |
|---|
| 120 |
if (args[programPos].length && args[programPos][0] != '-') break; |
|---|
| 121 |
} |
|---|
| 122 |
const |
|---|
| 123 |
root = /*rel2abs*/(chomp(args[programPos], ".d") ~ ".d"), |
|---|
| 124 |
exeBasename = basename(root, ".d"), |
|---|
| 125 |
exeDirname = dirname(root), |
|---|
| 126 |
programArgs = args[programPos + 1 .. $]; |
|---|
| 127 |
args = args[0 .. programPos]; |
|---|
| 128 |
const compilerFlags = args[1 .. programPos]; |
|---|
| 129 |
|
|---|
| 130 |
// Compute the object directory and ensure it exists |
|---|
| 131 |
invariant objDir = getObjPath(root, compilerFlags); |
|---|
| 132 |
if (!dryRun) // only make a fuss about objDir on a real run |
|---|
| 133 |
{ |
|---|
| 134 |
exists(objDir) |
|---|
| 135 |
? enforce(isdir(objDir), |
|---|
| 136 |
"Entry `"~objDir~"' exists but is not a directory.") |
|---|
| 137 |
: mkdir(objDir); |
|---|
| 138 |
} |
|---|
| 139 |
|
|---|
| 140 |
// Fetch dependencies |
|---|
| 141 |
const myModules = getDependencies(root, objDir, compilerFlags); |
|---|
| 142 |
|
|---|
| 143 |
// Compute executable name, check for freshness, rebuild |
|---|
| 144 |
if (exe) |
|---|
| 145 |
{ |
|---|
| 146 |
// user-specified exe name |
|---|
| 147 |
if (std.algorithm.endsWith(exe, std.path.sep[])) |
|---|
| 148 |
{ |
|---|
| 149 |
// user specified a directory, complete it to a file |
|---|
| 150 |
exe = std.path.join(exe, exeBasename); |
|---|
| 151 |
} |
|---|
| 152 |
} |
|---|
| 153 |
else |
|---|
| 154 |
{ |
|---|
| 155 |
//exe = exeBasename ~ '.' ~ hash(root, compilerFlags); |
|---|
| 156 |
version (Posix) |
|---|
| 157 |
exe = join(myOwnTmpDir, rel2abs(root)[1 .. $]) |
|---|
| 158 |
~ '.' ~ hash(root, compilerFlags); |
|---|
| 159 |
else version (Windows) |
|---|
| 160 |
exe = join(myOwnTmpDir, std.string.replace(root, ".", "-")) |
|---|
| 161 |
~ '-' ~ hash(root, compilerFlags); |
|---|
| 162 |
else |
|---|
| 163 |
assert(0); |
|---|
| 164 |
} |
|---|
| 165 |
// Add an ".exe" for Windows |
|---|
| 166 |
exe ~= binExt; |
|---|
| 167 |
|
|---|
| 168 |
// Have at it |
|---|
| 169 |
if (isNewer(root, exe) || |
|---|
| 170 |
std.algorithm.find! |
|---|
| 171 |
((string a) {return isNewer(a, exe);}) |
|---|
| 172 |
(myModules.keys).length) |
|---|
| 173 |
{ |
|---|
| 174 |
invariant result = rebuild(root, exe, objDir, myModules, compilerFlags, |
|---|
| 175 |
addStubMain); |
|---|
| 176 |
if (result) return result; |
|---|
| 177 |
} |
|---|
| 178 |
|
|---|
| 179 |
// run |
|---|
| 180 |
return buildOnly ? 0 : execv(exe, [ exe ] ~ programArgs); |
|---|
| 181 |
} |
|---|
| 182 |
|
|---|
| 183 |
bool inALibrary(in string source, in string object) |
|---|
| 184 |
{ |
|---|
| 185 |
// Heuristics: if source starts with "std.", it's in a library |
|---|
| 186 |
return std.string.startsWith(source, "std.") |
|---|
| 187 |
|| std.string.startsWith(source, "core.") |
|---|
| 188 |
|| source == "object" || source == "gcstats"; |
|---|
| 189 |
// another crude heuristic: if a module's path is absolute, it's |
|---|
| 190 |
// considered to be compiled in a separate library. Otherwise, |
|---|
| 191 |
// it's a source module. |
|---|
| 192 |
//return isabs(mod); |
|---|
| 193 |
} |
|---|
| 194 |
|
|---|
| 195 |
private string myOwnTmpDir() |
|---|
| 196 |
{ |
|---|
| 197 |
version (Posix) |
|---|
| 198 |
{ |
|---|
| 199 |
enum tmpRoot = "/tmp/.rdmd"; |
|---|
| 200 |
} |
|---|
| 201 |
else version (Windows) |
|---|
| 202 |
{ |
|---|
| 203 |
auto tmpRoot = std.process.getenv("TEMP"); |
|---|
| 204 |
if (!tmpRoot) |
|---|
| 205 |
{ |
|---|
| 206 |
tmpRoot = std.process.getenv("TMP"); |
|---|
| 207 |
} |
|---|
| 208 |
if (!tmpRoot) tmpRoot = join(".", ".rdmd"); |
|---|
| 209 |
else tmpRoot ~= sep ~ ".rdmd"; |
|---|
| 210 |
} |
|---|
| 211 |
exists(tmpRoot) && isdir(tmpRoot) || mkdirRecurse(tmpRoot); |
|---|
| 212 |
return tmpRoot; |
|---|
| 213 |
} |
|---|
| 214 |
|
|---|
| 215 |
private string hash(in string root, in string[] compilerFlags) |
|---|
| 216 |
{ |
|---|
| 217 |
enum string[] irrelevantSwitches = [ |
|---|
| 218 |
"--help", "-ignore", "-quiet", "-v" ]; |
|---|
| 219 |
MD5_CTX context; |
|---|
| 220 |
context.start(); |
|---|
| 221 |
context.update(getcwd); |
|---|
| 222 |
context.update(root); |
|---|
| 223 |
foreach (flag; compilerFlags) { |
|---|
| 224 |
if (find(irrelevantSwitches, flag).length) continue; |
|---|
| 225 |
context.update(flag); |
|---|
| 226 |
} |
|---|
| 227 |
ubyte digest[16]; |
|---|
| 228 |
context.finish(digest); |
|---|
| 229 |
return digestToString(digest); |
|---|
| 230 |
} |
|---|
| 231 |
|
|---|
| 232 |
private string getObjPath(in string root, in string[] compilerFlags) |
|---|
| 233 |
{ |
|---|
| 234 |
const tmpRoot = myOwnTmpDir; |
|---|
| 235 |
return std.path.join(tmpRoot, |
|---|
| 236 |
"rdmd-" ~ basename(root) ~ '-' ~ hash(root, compilerFlags)); |
|---|
| 237 |
} |
|---|
| 238 |
|
|---|
| 239 |
// Rebuild the executable fullExe starting from modules myModules |
|---|
| 240 |
// passing the compiler flags compilerFlags. Generates one large |
|---|
| 241 |
// object file. |
|---|
| 242 |
|
|---|
| 243 |
private int rebuild(string root, string fullExe, |
|---|
| 244 |
string objDir, in string[string] myModules, |
|---|
| 245 |
in string[] compilerFlags, bool addStubMain) |
|---|
| 246 |
{ |
|---|
| 247 |
auto todo = compiler~" "~join(compilerFlags, " ") |
|---|
| 248 |
~" -of"~shellQuote(fullExe) |
|---|
| 249 |
~" -od"~shellQuote(objDir) |
|---|
| 250 |
~" "~shellQuote(root)~" "; |
|---|
| 251 |
foreach (k; map!(shellQuote)(myModules.keys)) { |
|---|
| 252 |
todo ~= k ~ " "; |
|---|
| 253 |
} |
|---|
| 254 |
|
|---|
| 255 |
// Need to add the pesky void main(){}? |
|---|
| 256 |
if (addStubMain) |
|---|
| 257 |
{ |
|---|
| 258 |
auto stubMain = std.path.join(myOwnTmpDir, "stubmain.d"); |
|---|
| 259 |
std.file.write(stubMain, "void main(){}"); |
|---|
| 260 |
todo ~= stubMain; |
|---|
| 261 |
} |
|---|
| 262 |
|
|---|
| 263 |
invariant result = run(todo); |
|---|
| 264 |
if (result) |
|---|
| 265 |
{ |
|---|
| 266 |
// build failed |
|---|
| 267 |
return result; |
|---|
| 268 |
} |
|---|
| 269 |
// clean up the dir containing the object file |
|---|
| 270 |
rmdirRecurse(objDir); |
|---|
| 271 |
return 0; |
|---|
| 272 |
} |
|---|
| 273 |
|
|---|
| 274 |
// Run a program optionally writing the command line first |
|---|
| 275 |
|
|---|
| 276 |
private int run(string todo) |
|---|
| 277 |
{ |
|---|
| 278 |
if (chatty) writeln(todo); |
|---|
| 279 |
if (dryRun) return 0; |
|---|
| 280 |
return system(todo); |
|---|
| 281 |
} |
|---|
| 282 |
|
|---|
| 283 |
// Given module rootModule, returns a mapping of all dependees .d |
|---|
| 284 |
// source filenames to their corresponding .o files sitting in |
|---|
| 285 |
// directory objDir. The mapping is obtained by running dmd -v against |
|---|
| 286 |
// rootModule. |
|---|
| 287 |
|
|---|
| 288 |
private string[string] getDependencies(string rootModule, string objDir, |
|---|
| 289 |
in string[] compilerFlags) |
|---|
| 290 |
{ |
|---|
| 291 |
string d2obj(string dfile) { |
|---|
| 292 |
return std.path.join(objDir, chomp(basename(dfile), ".d")~objExt); |
|---|
| 293 |
} |
|---|
| 294 |
|
|---|
| 295 |
immutable depsFilename = rootModule~".deps"; |
|---|
| 296 |
immutable rootDir = dirname(rootModule); |
|---|
| 297 |
|
|---|
| 298 |
// myModules maps module source paths to corresponding .o names |
|---|
| 299 |
string[string] myModules;// = [ rootModule : d2obj(rootModule) ]; |
|---|
| 300 |
// Must collect dependencies |
|---|
| 301 |
invariant depsGetter = /*"cd "~shellQuote(rootDir)~" && " |
|---|
| 302 |
~*/compiler~" "~join(compilerFlags, " ") |
|---|
| 303 |
~" -v -o- "~shellQuote(rootModule) |
|---|
| 304 |
~" >"~depsFilename; |
|---|
| 305 |
if (chatty) writeln(depsGetter); |
|---|
| 306 |
immutable depsExitCode = system(depsGetter); |
|---|
| 307 |
if (depsExitCode) |
|---|
| 308 |
{ |
|---|
| 309 |
// if (exists(depsFilename)) |
|---|
| 310 |
// { |
|---|
| 311 |
// stderr.writeln(readText(depsFilename)); |
|---|
| 312 |
// } |
|---|
| 313 |
exit(depsExitCode); |
|---|
| 314 |
} |
|---|
| 315 |
auto depsReader = File(depsFilename); |
|---|
| 316 |
scope(exit) collectException(depsReader.close); // we don't care for errors |
|---|
| 317 |
|
|---|
| 318 |
// Fetch all dependent modules and append them to myModules |
|---|
| 319 |
auto pattern = new RegExp(r"^import\s+(\S+)\s+\((\S+)\)\s*$"); |
|---|
| 320 |
foreach (string line; lines(depsReader)) |
|---|
| 321 |
{ |
|---|
| 322 |
if (!pattern.test(line)) continue; |
|---|
| 323 |
invariant moduleName = pattern[1], moduleSrc = pattern[2]; |
|---|
| 324 |
if (inALibrary(moduleName, moduleSrc)) continue; |
|---|
| 325 |
invariant moduleObj = d2obj(moduleSrc); |
|---|
| 326 |
myModules[/*rel2abs*/join(rootDir, moduleSrc)] = moduleObj; |
|---|
| 327 |
} |
|---|
| 328 |
|
|---|
| 329 |
return myModules; |
|---|
| 330 |
} |
|---|
| 331 |
|
|---|
| 332 |
/*private*/ string shellQuote(string filename) |
|---|
| 333 |
{ |
|---|
| 334 |
// This may have to change under windows |
|---|
| 335 |
version (Windows) enum quotechar = '"'; |
|---|
| 336 |
else enum quotechar = '\''; |
|---|
| 337 |
return quotechar ~ filename ~ quotechar; |
|---|
| 338 |
} |
|---|
| 339 |
|
|---|
| 340 |
private bool isNewer(string source, string target) |
|---|
| 341 |
{ |
|---|
| 342 |
return force || lastModified(source) >= lastModified(target, d_time.min); |
|---|
| 343 |
} |
|---|
| 344 |
|
|---|
| 345 |
private string helpString() |
|---|
| 346 |
{ |
|---|
| 347 |
return |
|---|
| 348 |
"rdmd build "~thisVersion~" |
|---|
| 349 |
Usage: rdmd [RDMD AND DMD OPTIONS]... program [PROGRAM OPTIONS]... |
|---|
| 350 |
Builds (with dependents) and runs a D program. |
|---|
| 351 |
Example: rdmd -release myprog --myprogparm 5 |
|---|
| 352 |
|
|---|
| 353 |
Any option to be passed to dmd must occur before the program name. In addition |
|---|
| 354 |
to dmd options, rdmd recognizes the following options: |
|---|
| 355 |
--build-only just build the executable, don't run it |
|---|
| 356 |
--chatty write dmd commands to stdout before executing them |
|---|
| 357 |
--compiler=comp use the specified compiler (e.g. gdmd) instead of dmd |
|---|
| 358 |
--dry-run do not compile, just show what commands would be run |
|---|
| 359 |
(implies --chatty) |
|---|
| 360 |
--eval=code evaluate code \u00E0 la perl -e (multiple --eval allowed) |
|---|
| 361 |
--force force a rebuild even if apparently not necessary |
|---|
| 362 |
--help this message |
|---|
| 363 |
--loop assume \"foreach (line; stdin.byLine()) { ... }\" for eval |
|---|
| 364 |
--main add a stub main program to the mix (e.g. for unittesting) |
|---|
| 365 |
--man open web browser on manual page |
|---|
| 366 |
--shebang rdmd is in a shebang line (put as first argument) |
|---|
| 367 |
"; |
|---|
| 368 |
} |
|---|
| 369 |
|
|---|
| 370 |
// For --eval |
|---|
| 371 |
immutable string importWorld = " |
|---|
| 372 |
module temporary; |
|---|
| 373 |
import std.stdio, std.algorithm, std.array, std.atomics, std.base64, |
|---|
| 374 |
std.bigint, /*std.bind, std.bitarray,*/ std.bitmanip, std.boxer, |
|---|
| 375 |
std.compiler, std.complex, std.contracts, std.conv, std.cpuid, std.cstream, |
|---|
| 376 |
std.ctype, std.date, std.dateparse, std.demangle, std.encoding, std.file, |
|---|
| 377 |
std.format, std.functional, std.getopt, std.intrinsic, std.iterator, |
|---|
| 378 |
/*std.loader,*/ std.math, std.md5, std.metastrings, std.mmfile, |
|---|
| 379 |
std.numeric, std.outbuffer, std.path, std.perf, std.process, |
|---|
| 380 |
std.random, std.range, std.regex, std.regexp, std.signals, std.socket, |
|---|
| 381 |
std.socketstream, std.stdint, std.stdio, std.stdiobase, std.stream, |
|---|
| 382 |
std.string, std.syserror, std.system, std.traits, std.typecons, |
|---|
| 383 |
std.typetuple, std.uni, std.uri, std.utf, std.variant, std.xml, std.zip, |
|---|
| 384 |
std.zlib; |
|---|
| 385 |
"; |
|---|
| 386 |
|
|---|
| 387 |
int eval(string todo) |
|---|
| 388 |
{ |
|---|
| 389 |
MD5_CTX context; |
|---|
| 390 |
context.start(); |
|---|
| 391 |
context.update(todo); |
|---|
| 392 |
ubyte digest[16]; |
|---|
| 393 |
context.finish(digest); |
|---|
| 394 |
auto pathname = myOwnTmpDir; |
|---|
| 395 |
auto progname = std.path.join(pathname, |
|---|
| 396 |
"eval." ~ digestToString(digest)); |
|---|
| 397 |
|
|---|
| 398 |
if (exists(progname) || |
|---|
| 399 |
// Compile it |
|---|
| 400 |
(std.file.write(progname~".d", todo), |
|---|
| 401 |
run("dmd " ~ progname ~ ".d -of" ~ progname) == 0)) |
|---|
| 402 |
{ |
|---|
| 403 |
// It's there, just run it |
|---|
| 404 |
run(progname); |
|---|
| 405 |
} |
|---|
| 406 |
|
|---|
| 407 |
// Clean pathname |
|---|
| 408 |
enum lifetimeInHours = 24; |
|---|
| 409 |
auto cutoff = getUTCtime - 60 * 60 * lifetimeInHours * ticksPerSecond; |
|---|
| 410 |
foreach (DirEntry d; dirEntries(pathname, SpanMode.shallow)) |
|---|
| 411 |
{ |
|---|
| 412 |
if (d.lastWriteTime < cutoff) |
|---|
| 413 |
{ |
|---|
| 414 |
std.file.remove(d.name); |
|---|
| 415 |
//break; // only one per call so we don't waste time |
|---|
| 416 |
} |
|---|
| 417 |
} |
|---|
| 418 |
|
|---|
| 419 |
return 0; |
|---|
| 420 |
} |
|---|
| 421 |
|
|---|
| 422 |
string thisVersion() |
|---|
| 423 |
{ |
|---|
| 424 |
enum d = __DATE__; |
|---|
| 425 |
enum month = d[0 .. 3], |
|---|
| 426 |
day = d[4] == ' ' ? "0"~d[5] : d[4 .. 6], |
|---|
| 427 |
year = d[7 .. $]; |
|---|
| 428 |
enum monthNum |
|---|
| 429 |
= month == "Jan" ? "01" |
|---|
| 430 |
: month == "Feb" ? "02" |
|---|
| 431 |
: month == "Mar" ? "03" |
|---|
| 432 |
: month == "Apr" ? "04" |
|---|
| 433 |
: month == "May" ? "05" |
|---|
| 434 |
: month == "Jun" ? "06" |
|---|
| 435 |
: month == "Jul" ? "07" |
|---|
| 436 |
: month == "Aug" ? "08" |
|---|
| 437 |
: month == "Sep" ? "09" |
|---|
| 438 |
: month == "Oct" ? "10" |
|---|
| 439 |
: month == "Nov" ? "11" |
|---|
| 440 |
: month == "Dec" ? "12" |
|---|
| 441 |
: ""; |
|---|
| 442 |
static assert(month != "", "Unknown month "~month); |
|---|
| 443 |
return year[0]~year[1 .. $]~monthNum~day; |
|---|
| 444 |
} |
|---|
| 445 |
|
|---|
| 446 |
/* |
|---|
| 447 |
* Copyright (C) 2008 by Andrei Alexandrescu |
|---|
| 448 |
* Written by Andrei Alexandrescu, www.erdani.org |
|---|
| 449 |
* Based on an idea by Georg Wrede |
|---|
| 450 |
* Featuring improvements suggested by Christopher Wright |
|---|
| 451 |
* Windows port using bug fixes and suggestions by Adam Ruppe |
|---|
| 452 |
* |
|---|
| 453 |
* This software is provided 'as-is', without any express or implied |
|---|
| 454 |
* warranty. In no event will the authors be held liable for any damages |
|---|
| 455 |
* arising from the use of this software. |
|---|
| 456 |
* |
|---|
| 457 |
* Permission is granted to anyone to use this software for any purpose, |
|---|
| 458 |
* including commercial applications, and to alter it and redistribute it |
|---|
| 459 |
* freely, subject to the following restrictions: |
|---|
| 460 |
* |
|---|
| 461 |
* o The origin of this software must not be misrepresented; you must not |
|---|
| 462 |
* claim that you wrote the original software. If you use this software |
|---|
| 463 |
* in a product, an acknowledgment in the product documentation would be |
|---|
| 464 |
* appreciated but is not required. |
|---|
| 465 |
* o Altered source versions must be plainly marked as such, and must not |
|---|
| 466 |
* be misrepresented as being the original software. |
|---|
| 467 |
* o This notice may not be removed or altered from any source |
|---|
| 468 |
* distribution. |
|---|
| 469 |
*/ |
|---|