app.d 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478
  1. import std.stdio;
  2. import std.file;
  3. import std.path;
  4. import std.compiler;
  5. import std.system;
  6. import std.format;
  7. import std.conv;
  8. import std.process;
  9. import std.string;
  10. import iopipe.json.serialize;
  11. import iopipe.json.parser;
  12. import iopipe.json.dom : JSONValue;
  13. import std.exception;
  14. import iopipe.traits;
  15. import schlib.getopt2;
  16. import std.getopt: defaultGetoptPrinter;
  17. // tar support copied from arsd.archive: https://github.com/adamdruppe/arsd
  18. // Written by Adam D. Ruppe
  19. /++
  20. A header of a file in the archive. This represents the
  21. binary format of the header block.
  22. +/
  23. align(512)
  24. struct TarFileHeader {
  25. align(1):
  26. char[100] fileName_ = 0;
  27. char[8] fileMode_ = 0;
  28. char[8] ownerUid_ = 0;
  29. char[8] ownerGid_ = 0;
  30. char[12] size_ = 0; // in octal
  31. char[12] mtime_ = 0; // octal unix timestamp
  32. char[8] checksum_ = 0; // right?????
  33. char[1] fileType_ = 0; // hard link, soft link, etc
  34. char[100] linkFileName_ = 0;
  35. char[6] ustarMagic_ = 0; // if "ustar\0", remaining fields are set
  36. char[2] ustarVersion_ = 0;
  37. char[32] ownerName_ = 0;
  38. char[32] groupName_ = 0;
  39. char[8] deviceMajorNumber_ = 0;
  40. char[8] deviceMinorNumber_ = 0;
  41. char[155] filenamePrefix_ = 0;
  42. /// Returns the filename. You should cache the return value as long as TarFileHeader is in scope (it returns a slice after calling strlen)
  43. const(char)[] filename() {
  44. import core.stdc.string;
  45. if(filenamePrefix_[0])
  46. return upToZero(filenamePrefix_[]) ~ upToZero(fileName_[]);
  47. return upToZero(fileName_[]);
  48. }
  49. ///
  50. ulong size() {
  51. import core.stdc.stdlib;
  52. return strtoul(size_.ptr, null, 8);
  53. }
  54. ///
  55. TarFileType type() {
  56. if(fileType_[0] == 0)
  57. return TarFileType.normal;
  58. else
  59. return cast(TarFileType) (fileType_[0] - '0');
  60. }
  61. uint mode() {
  62. import std.conv : to;
  63. return fileMode_.upToZero.to!int(8);
  64. }
  65. }
  66. /// There's other types but this is all I care about. You can still detect the char by `((cast(char) type) + '0')`
  67. enum TarFileType {
  68. normal = 0, ///
  69. hardLink = 1, ///
  70. symLink = 2, ///
  71. characterSpecial = 3, ///
  72. blockSpecial = 4, ///
  73. directory = 5, ///
  74. fifo = 6 ///
  75. }
  76. /++
  77. Low level tar file processor. You must pass it a
  78. TarFileHeader buffer as well as a size_t for context.
  79. Both must be initialized to all zeroes on first call,
  80. then not modified in between calls.
  81. Each call must populate the dataBuffer with 512 bytes.
  82. returns true if still work to do.
  83. +/
  84. bool processTar(
  85. TarFileHeader* header,
  86. long* bytesRemainingOnCurrentFile,
  87. ubyte[] dataBuffer,
  88. scope void delegate(TarFileHeader* header, bool isNewFile, bool fileFinished, ubyte[] data) handleData
  89. )
  90. {
  91. assert(dataBuffer.length == 512);
  92. assert(bytesRemainingOnCurrentFile !is null);
  93. assert(header !is null);
  94. if(*bytesRemainingOnCurrentFile) {
  95. bool isNew = *bytesRemainingOnCurrentFile == header.size();
  96. if(*bytesRemainingOnCurrentFile <= 512) {
  97. handleData(header, isNew, true, dataBuffer[0 .. cast(size_t) *bytesRemainingOnCurrentFile]);
  98. *bytesRemainingOnCurrentFile = 0;
  99. } else {
  100. handleData(header, isNew, false, dataBuffer[]);
  101. *bytesRemainingOnCurrentFile -= 512;
  102. }
  103. } else {
  104. *header = *(cast(TarFileHeader*) dataBuffer.ptr);
  105. auto s = header.size();
  106. *bytesRemainingOnCurrentFile = s;
  107. if(header.type() == TarFileType.symLink)
  108. handleData(header, true, true, cast(ubyte[])header.linkFileName_.upToZero);
  109. if(header.type() == TarFileType.directory)
  110. handleData(header, true, false, null);
  111. if(s == 0 && header.type == TarFileType.normal)
  112. return false;
  113. }
  114. return true;
  115. }
  116. T[] upToZero(T)(T[] input)
  117. {
  118. foreach(i, v; input)
  119. if(v == 0)
  120. return input[0 .. i];
  121. return input;
  122. }
  123. version(X86)
  124. enum arch="x86";
  125. else version(X86_64)
  126. enum arch="x86_64";
  127. else version(ARM)
  128. enum arch="arm";
  129. else version(AArch64)
  130. enum arch="arm64";
  131. else
  132. static assert(false, "Unsupported architecture");
  133. // establish the runtime
  134. version(CRuntime_Microsoft)
  135. enum CRT="MSVC";
  136. else version(CRuntime_Glibc)
  137. enum CRT="glibc";
  138. else version(CppRuntime_Clang)
  139. enum CRT="llvm";
  140. else
  141. static assert(false, "Unsupported runtime");
  142. enum osStr = os.to!string;
  143. enum baseDir = buildPath("install", "lib", osStr, arch, CRT);
  144. auto writeTo(C, S)(C chain, S sink)
  145. {
  146. import iopipe.bufpipe;
  147. import iopipe.valve;
  148. return chain.push!(c => c.outputPipe(sink));
  149. }
  150. void extractArchive(char[] path)
  151. {
  152. import std.io : File, mode;
  153. import iopipe.bufpipe;
  154. import iopipe.refc;
  155. import iopipe.zip;
  156. auto archivePath = buildPath(path, "install", "lib.tgz");
  157. auto expectedPrefix = "lib/" ~ osStr ~ "/";
  158. enforce(exists(archivePath), "No lib archive found at " ~ archivePath ~ "!");
  159. // the input file
  160. auto inputFile = File(archivePath, mode!"rb").refCounted.bufd.unzip;
  161. bool doOutput;
  162. // open a file using iopipe
  163. auto openOutputFile(string fname)
  164. {
  165. return bufd.writeTo(File(fname, mode!"wb").refCounted);
  166. }
  167. typeof(openOutputFile("")) currentFile;
  168. string currentSymlinkText;
  169. void handleTar(TarFileHeader *header, bool isNewFile, bool fileFinished, ubyte[] data)
  170. {
  171. auto ft = header.type;
  172. if(isNewFile)
  173. {
  174. // check that the name matches
  175. auto fn = header.filename;
  176. if(!fn.startsWith(expectedPrefix))
  177. return;
  178. version(Posix)
  179. {
  180. // handle symlinks on posix
  181. if(ft == TarFileType.symLink)
  182. {
  183. doOutput = true;
  184. currentSymlinkText = "";
  185. }
  186. }
  187. if(ft == TarFileType.normal)
  188. {
  189. doOutput = true;
  190. auto newFilePath = buildPath(path, "install", fn);
  191. mkdirRecurse(dirName(newFilePath));
  192. currentFile = openOutputFile(newFilePath);
  193. }
  194. }
  195. if(doOutput)
  196. {
  197. if(ft == TarFileType.symLink)
  198. currentSymlinkText ~= cast(char[])data;
  199. else
  200. {
  201. currentFile.ensureElems(data.length);
  202. assert(currentFile.window.length >= data.length);
  203. currentFile.window[0 .. data.length] = data[];
  204. currentFile.release(data.length);
  205. }
  206. }
  207. if(fileFinished)
  208. {
  209. if(doOutput)
  210. {
  211. auto fn = header.filename;
  212. auto fp = buildPath(path, "install", fn);
  213. version(Posix)
  214. {
  215. if(ft == TarFileType.symLink)
  216. {
  217. mkdirRecurse(dirName(fp));
  218. symlink(currentSymlinkText, fp);
  219. }
  220. }
  221. if(ft == TarFileType.normal)
  222. {
  223. // close the file
  224. destroy(currentFile);
  225. // Effect the correct file permissions
  226. version(Posix)
  227. {
  228. setAttributes(fp, header.mode);
  229. }
  230. }
  231. doOutput = false;
  232. }
  233. }
  234. }
  235. // for tar
  236. TarFileHeader tfh;
  237. long size;
  238. while(inputFile.extend(0) > 0)
  239. {
  240. while(inputFile.window.length >= 512)
  241. {
  242. // big enough to process another tar chunk
  243. processTar(&tfh, &size, inputFile.window[0 .. 512], &handleTar);
  244. inputFile.release(512);
  245. }
  246. }
  247. }
  248. int main(string[] args)
  249. {
  250. enum UpdateJson {
  251. ask,
  252. yes,
  253. no
  254. }
  255. struct Opts {
  256. @description("Should the install script update your dub.json file? yes/no/ask")
  257. @shortname("u")
  258. UpdateJson updateJson = UpdateJson.ask;
  259. @description("Suppress all stdout, and most stderr output. Good for scripts")
  260. @shortname("q")
  261. bool quiet = false;
  262. }
  263. Opts opts;
  264. auto optResult = args.getopt2(opts);
  265. if(optResult.helpWanted)
  266. {
  267. defaultGetoptPrinter(
  268. `Install a binary library for use with linking to raylib. This program uses your dub configuration
  269. and OS to determine the correct library files to copy, and places them in your project directory.
  270. Your program must have raylib-d as a dependency. It optionally can update your dub.json file to
  271. include linker directives for building your program. This script can be used in build scripts by
  272. passing a parameter to not update the dub.json file.`, optResult.options);
  273. return 1;
  274. }
  275. auto notify(Args...)(Args args)
  276. {
  277. if(!opts.quiet)
  278. return writefln(args);
  279. }
  280. notify("raylib-d library installation");
  281. // look at the dub.selections.json file
  282. try {
  283. auto dubConfig = execute(["dub", "describe"], null, Config.stderrPassThrough);
  284. string raylibdPath;
  285. if(dubConfig.status != 0)
  286. {
  287. if(!opts.quiet)
  288. stderr.writeln("Error executing dub describe");
  289. return dubConfig.status;
  290. }
  291. char[] getRaylibPath(char[] jsonStr)
  292. {
  293. auto tokens = jsonTokenizer(jsonStr);
  294. enforce(tokens.parseTo("packages"), "Could not find packages in dub json output!");
  295. auto nt = tokens.next.token;
  296. enforce(nt == JSONToken.ArrayStart, "Expected array start in packages");
  297. while(nt != JSONToken.ArrayEnd)
  298. {
  299. tokens.releaseParsed();
  300. tokens.startCache;
  301. enforce(tokens.parseTo("name"), "Could not find package name in json file");
  302. auto n = tokens.next;
  303. jsonExpect(n, JSONToken.String, "Expected string for package name");
  304. if(n.data(tokens.chain) == "raylib-d")
  305. {
  306. tokens.rewind;
  307. tokens.parseTo("path");
  308. auto p = tokens.next;
  309. jsonExpect(p, JSONToken.String, "Expected string for path");
  310. return p.data(tokens.chain);
  311. }
  312. tokens.rewind;
  313. tokens.endCache;
  314. nt = tokens.skipItem;
  315. // skip whatever the next token is, it's either a comma, or the array end.
  316. cast(void)tokens.next;
  317. }
  318. throw new Exception("Could not find raylib-d dependency for current project!");
  319. }
  320. try {
  321. auto path = getRaylibPath(dubConfig.output.dup);
  322. // check to see if the `lib` directory exists, and if not, see if we can extract it from a tarball
  323. notify("Detected raylib dependency path as %s", path);
  324. auto libpath = buildPath(path, baseDir);
  325. notify("Copying library files from %s", libpath);
  326. if(!exists(libpath))
  327. {
  328. // extract the data, but only for the detected OS
  329. notify("Library path does not exist, trying archive");
  330. extractArchive(path);
  331. }
  332. foreach(ent; dirEntries(libpath, SpanMode.shallow))
  333. {
  334. auto newLoc = buildPath(".", ent.name.baseName(".lnk"));
  335. if(exists(newLoc))
  336. {
  337. notify("Skipping existing file %s", newLoc);
  338. continue;
  339. }
  340. version(Posix)
  341. {
  342. void makeLink(string origln)
  343. {
  344. notify("Creating symlink %s -> %s", newLoc, origln);
  345. symlink(origln, newLoc);
  346. }
  347. if(ent.isSymlink)
  348. {
  349. // recreate the symlink
  350. makeLink(readLink(ent.name));
  351. continue;
  352. }
  353. else if(ent.name.endsWith(".lnk"))
  354. {
  355. // dub workaround. This is really a symlink but wasn't
  356. // properly downloaded by dub.
  357. makeLink(readText(ent.name));
  358. continue;
  359. }
  360. }
  361. notify("Installing library file %s", newLoc);
  362. copy(ent.name, newLoc, PreserveAttributes.yes);
  363. }
  364. } catch(Exception ex) {
  365. if(!opts.quiet)
  366. stderr.writeln("Error: ", ex.msg);
  367. return 1;
  368. }
  369. // display what to put in the json or sdl file
  370. if(exists("dub.json"))
  371. {
  372. import std.io : File, mode;
  373. import iopipe.bufpipe;
  374. import iopipe.refc;
  375. import iopipe.textpipe;
  376. import std.algorithm : canFind;
  377. // json, we can handle. Read the file and check for the correct flags
  378. static struct DubFile {
  379. @optional:
  380. string[] libs;
  381. @alternateName("lflags-posix") string[] lflagsPosix;
  382. @alternateName("lflags-osx") string[] lflagsOsx;
  383. @alternateName("lflags-linux") string[] lflagsLinux;
  384. @extras JSONValue!string _extras;
  385. }
  386. auto dubfile = File("dub.json", mode!"rb").refCounted.bufd.assumeText.deserialize!DubFile;
  387. // check to see that all the proper things are present
  388. bool hasLib = dubfile.libs.canFind("raylib");
  389. bool hasPosixFlags = dubfile.lflagsPosix.canFind("-L.");
  390. bool hasOsxFlags = dubfile.lflagsOsx.canFind(["-rpath", "@executable_path/"]);
  391. bool hasLinuxFlags = dubfile.lflagsLinux.canFind("-rpath=$$ORIGIN");
  392. if(!hasLib || !hasPosixFlags || !hasOsxFlags || !hasLinuxFlags)
  393. {
  394. if(opts.updateJson == UpdateJson.ask)
  395. {
  396. writeln(
  397. `Proper dub linker directives missing, the following directives in dub.json will link the installed raylib library:
  398. "libs": [ "raylib" ],
  399. "lflags-posix" : ["-L."],
  400. "lflags-osx" : ["-rpath", "@executable_path/"],
  401. "lflags-linux" : ["-rpath=$$ORIGIN"]`);
  402. // ask the user if they want to update the dub.json file with the
  403. // correct directives.
  404. std.stdio.write("Automatically add these directives to your dub.json file? [Y/n] ");
  405. stdout.flush();
  406. auto result = readln().strip;
  407. if(result == "Y" || result == "y" || result == "")
  408. opts.updateJson = UpdateJson.yes;
  409. }
  410. if(opts.updateJson == UpdateJson.yes)
  411. {
  412. // add the correct directives
  413. if(!hasLib)
  414. dubfile.libs ~= "raylib";
  415. if(!hasPosixFlags)
  416. dubfile.lflagsPosix ~= "-L.";
  417. if(!hasOsxFlags)
  418. dubfile.lflagsOsx ~= ["-rpath", "@executable_path/"];
  419. if(!hasLinuxFlags)
  420. dubfile.lflagsLinux ~= "-rpath=$$ORIGIN";
  421. // rewrite the file
  422. std.file.write("dub.json", serialize(dubfile));
  423. notify("dub.json file updated!");
  424. }
  425. }
  426. }
  427. else if(exists("dub.sdl"))
  428. {
  429. notify(
  430. `If not already present, the following directives in dub.sdl will link the installed raylib library:
  431. libs "raylib"
  432. lflags "-rpath" "@executable_path/" platform="osx"
  433. lflags "-L." platform="posix"
  434. lflags "-rpath=$$ORIGIN" platform="linux"`);
  435. }
  436. }
  437. catch(Throwable t)
  438. {
  439. // suppress any output
  440. if(opts.quiet) return 1;
  441. }
  442. return 0;
  443. }