1 module dbuild.src; 2 3 import std.digest.md : MD5; 4 5 /// Source code interface 6 interface Source 7 { 8 /// Feeds the digest in a way that makes a unique build identifier. 9 void feedBuildId(ref MD5 digest); 10 11 /// Obtain the source code. 12 /// Implementation for local directories will simply return the local source dir, 13 /// without consideration of the workDir. 14 /// Implementation of source fetching (archive or scm) will download and 15 /// extract (or clone) the source tree into workDir. 16 /// Params: 17 /// workDir = work directory that should preferably be used to download 18 /// and extract the source code. 19 /// Returns: the path to the src root directory. 20 string obtain(in string workDir); 21 22 /// Add patches to the source code. 23 /// Patches are patch content, not patch filenames. 24 /// Patches are applied with Git which must be found in PATH 25 /// (even if source code is not a Git repo) 26 final Source withPatch(in string[] patches) 27 { 28 return new PatchedSource(this, patches); 29 } 30 } 31 32 /// Returns a Source pointing to a local existing directory 33 Source localSource(in string dir) 34 { 35 return new LocalSource(dir); 36 } 37 38 /// Returns a Source pointing to a local directory within $DUB_PACKAGE 39 Source dubPkgSource(in string subdir) 40 { 41 import std.path : buildPath; 42 import std.process : environment; 43 44 return new LocalSource(buildPath(environment["DUB_PACKAGE"], subdir)); 45 } 46 47 /// Returns a Source that fetches and extract an archive into the work directory 48 /// md5 can be specified with an md5 checksum to check integrity of the downloaded 49 /// archive 50 Source archiveFetchSource(in string url, in string md5=null) 51 { 52 import std.exception : enforce; 53 54 enforce(isSupportedArchiveExt(url), url~" is not a supported archive type"); 55 return new ArchiveFetchSource(url, md5); 56 } 57 58 /// Returns a Source that will clone a git repo and checkout a specified commit. 59 /// commitRef can be a branch, tag, or commit hash. Anything `git checkout` will understand. 60 Source gitSource(in string url, in string commitRef) 61 { 62 return new GitSource(url, commitRef); 63 } 64 65 private class LocalSource : Source 66 { 67 string dir; 68 this (in string dir) 69 { 70 import std.exception : enforce; 71 import std.file : exists, isDir; 72 73 enforce(exists(dir) && isDir(dir), dir~": no such directory!"); 74 this.dir = dir; 75 } 76 77 override void feedBuildId(ref MD5 digest) 78 { 79 import dbuild.util : feedDigestData; 80 81 feedDigestData(digest, dir); 82 } 83 84 string obtain(in string) 85 { 86 return this.dir; 87 } 88 } 89 90 private string srcLockPath(in string workDir) 91 { 92 import std.path : buildPath; 93 94 return buildPath(workDir, ".srcLock"); 95 } 96 97 private string patchLockPath(in string workDir, in string patch) 98 { 99 import dbuild.util : feedDigestData; 100 import std.digest : toHexString, LetterCase; 101 import std.digest.md : MD5; 102 import std.path : buildPath; 103 104 MD5 md5; 105 feedDigestData(md5, patch); 106 const binHash = md5.finish(); 107 const hash = toHexString!(LetterCase.lower)(binHash)[0 .. 7].idup; 108 return buildPath(workDir, "."~hash~".patchLock"); 109 } 110 111 private class ArchiveFetchSource : Source 112 { 113 string url; 114 string md5; 115 this (in string url, in string md5) 116 { 117 this.url = url; 118 this.md5 = md5; 119 } 120 121 override void feedBuildId(ref MD5 digest) 122 { 123 import dbuild.util : feedDigestData; 124 125 feedDigestData(digest, url); 126 feedDigestData(digest, md5); 127 } 128 129 override string obtain(in string workDir) 130 { 131 import dbuild.util : lockFile; 132 import std.file : exists, isDir; 133 import std.path : buildPath; 134 import std.uri : decode; 135 136 const decoded = decode(url); 137 const fn = urlLastComp(decoded); 138 const archive = buildPath(workDir, fn); 139 140 const ldn = likelySrcDirName(archive); 141 if (exists(ldn) && isDir(ldn)) { 142 return ldn; 143 } 144 145 auto lock = lockFile(srcLockPath(workDir)); 146 downloadArchive(archive); 147 return extractArchive(archive, workDir); 148 } 149 150 private void downloadArchive(in string archive) 151 { 152 import dbuild.util : checkMd5; 153 import std.exception : enforce; 154 import std.file : exists; 155 import std.net.curl : download; 156 import std.stdio : writefln; 157 158 if (!exists(archive) || !(md5.length && checkMd5(archive, md5))) { 159 if (exists(archive)) { 160 import std.file : remove; 161 remove(archive); 162 } 163 writefln("downloading %s", url); 164 download(url, archive); 165 166 enforce(!md5.length || checkMd5(archive, md5), "wrong md5 sum for "~archive); 167 } 168 } 169 170 private string extractArchive(in string archive, in string workDir) 171 { 172 import std.file : remove; 173 174 final switch(archiveFormat(archive)) { 175 case ArchiveFormat.targz: 176 const tarF = extractTarGz(archive); 177 const res = extractTar(tarF, workDir); 178 remove(tarF); 179 return res; 180 case ArchiveFormat.tar: 181 return extractTar(archive, workDir); 182 case ArchiveFormat.zip: 183 return extractZip(archive, workDir); 184 } 185 } 186 187 /// extract .tar.gz to .tar, returns the .tar file path 188 private string extractTarGz(const string archive) 189 in { assert(archive[$-7 .. $] == ".tar.gz"); } 190 out(res) { assert(res[$-4 .. $] == ".tar"); } 191 body { 192 import std.algorithm : map; 193 import std.exception : enforce; 194 import std.file : exists, remove; 195 import std.stdio : File, writefln; 196 import std.zlib : UnCompress; 197 198 writefln("extracting %s", archive); 199 200 const tarFn = archive[0 .. $-3]; 201 enforce(!exists(tarFn), tarFn~" already exists"); 202 203 auto inF = File(archive, "rb"); 204 auto outF = File(tarFn, "wb"); 205 206 UnCompress decmp = new UnCompress; 207 foreach (chunk; inF.byChunk(4096).map!(x => decmp.uncompress(x))) 208 { 209 outF.rawWrite(chunk); 210 } 211 212 return tarFn; 213 } 214 215 private string extractTar(in string archive, in string workDir) 216 { 217 import dbuild.tar : extractTo, isSingleRootDir; 218 import std.exception : enforce; 219 import std.file : exists, isDir; 220 import std.path : buildPath; 221 import std.stdio : writefln; 222 223 writefln("extracting %s", archive); 224 225 string extractDir; 226 string srcDir; 227 string rootDir; 228 if (isSingleRootDir(archive, rootDir)) { 229 extractDir = workDir; 230 srcDir = buildPath(workDir, rootDir); 231 } 232 else { 233 extractDir = buildPath(workDir, "src"); 234 srcDir = extractDir; 235 } 236 237 // trusting src dir content? 238 if (!exists(srcDir)) { 239 extractTo(archive, extractDir); 240 } 241 242 enforce(isDir(srcDir)); 243 return srcDir; 244 } 245 246 private string extractZip(in string archive, in string workDir) 247 { 248 import std.digest.crc : crc32Of; 249 import std.exception : enforce; 250 import std.file : exists, isDir, mkdirRecurse, read, write; 251 import std.path : buildNormalizedPath, buildPath, dirName, pathSplitter; 252 import std.stdio : writeln, writefln; 253 import std.zip : ZipArchive; 254 255 writefln("extracting %s", archive); 256 auto zip = new ZipArchive(read(archive)); 257 string extractDir; 258 string srcDir; 259 string rootDir; 260 bool singleRoot = true; 261 262 foreach(n, m; zip.directory) { 263 const dir = pathSplitter(n).front; 264 if (rootDir && dir != rootDir) { 265 singleRoot = false; 266 break; 267 } 268 if (!rootDir) rootDir = dir; 269 } 270 if (singleRoot) { 271 extractDir = workDir; 272 srcDir = buildPath(workDir, rootDir); 273 } 274 else { 275 extractDir = buildPath(workDir, "src"); 276 srcDir = extractDir; 277 } 278 279 foreach(n, m; zip.directory) { 280 const file = buildNormalizedPath(extractDir, n); 281 if ((exists(file) && isDir(file)) || m.expandedSize == 0) continue; 282 mkdirRecurse(dirName(file)); 283 zip.expand(m); 284 enforce(m.expandedData.length == cast(size_t)m.expandedSize, "zip data does not have expected size"); 285 const crc32 = crc32Of(m.expandedData); 286 const crc32_ = *(cast(const(uint)*)&crc32[0]); 287 enforce(crc32_ == m.crc32, "CRC32 zip check failed"); 288 write(file, m.expandedData); 289 } 290 291 return srcDir; 292 } 293 } 294 295 private class GitSource : Source 296 { 297 string url; 298 string commitRef; 299 300 this (in string url, in string commitRef) 301 { 302 import dbuild.util : searchExecutable; 303 import std.exception : enforce; 304 305 enforce(searchExecutable("git"), "could not find git in PATH!"); 306 this.url = url; 307 this.commitRef = commitRef; 308 } 309 310 override void feedBuildId(ref MD5 digest) 311 { 312 import dbuild.util : feedDigestData; 313 314 feedDigestData(digest, url); 315 feedDigestData(digest, commitRef); 316 } 317 override string obtain(in string workDir) 318 { 319 import dbuild.util : runCommand; 320 import std.algorithm : endsWith; 321 import std.exception : enforce; 322 import std.file : exists, isDir; 323 import std.path : buildPath; 324 import std.process : pipeProcess, Redirect; 325 import std.uri : decode; 326 327 enforce(commitRef.length, "must specify commitRef for git checkout"); 328 329 const decoded = decode(url); 330 auto dirName = urlLastComp(decoded); 331 if (dirName.endsWith(".git")) { 332 dirName = dirName[0 .. $-4]; 333 } 334 335 const srcDir = buildPath(workDir, dirName); 336 337 if (!exists(srcDir)) { 338 runCommand(["git", "clone", url, dirName], workDir, false); 339 } 340 341 enforce(isDir(srcDir)); 342 343 runCommand(["git", "checkout", commitRef], srcDir, false); 344 345 return srcDir; 346 } 347 } 348 349 private class PatchedSource : Source 350 { 351 const(string)[] patches; 352 Source impl; 353 354 this(Source impl, in string[] patches) 355 { 356 import dbuild.util : searchExecutable; 357 import std.exception : enforce; 358 359 enforce(searchExecutable("git"), "could not find git in PATH! Git is needed to apply patche"); 360 this.impl = impl; 361 this.patches = patches; 362 } 363 364 void feedBuildId(ref MD5 digest) 365 { 366 import dbuild.util : feedDigestData; 367 368 impl.feedBuildId(digest); 369 feedDigestData(digest, patches); 370 } 371 372 string obtain(in string workDir) 373 { 374 import dbuild.util : lockFile; 375 import std.process : Config, pipeProcess, Redirect, wait; 376 import std.stdio : writefln; 377 import std.file : exists; 378 379 const dir = impl.obtain(workDir); 380 381 writefln("Patching %s", dir); 382 383 foreach (patch; patches) { 384 const lockPath = patchLockPath(workDir, patch); 385 if (exists(lockPath)) { 386 continue; 387 } 388 const args = ["git", "apply", "-"]; 389 auto pipes = pipeProcess(args, Redirect.stderr|Redirect.stdin, null, Config.none, dir); 390 pipes.stdin.write(patch); 391 pipes.stdin.flush(); 392 pipes.stdin.close(); 393 wait(pipes.pid); 394 auto lock = lockFile(lockPath); 395 } 396 397 return dir; 398 } 399 } 400 401 private enum ArchiveFormat 402 { 403 targz, tar, zip, 404 } 405 406 private immutable(string[]) supportedArchiveExts = [ 407 ".zip", ".tar.gz", ".tar" 408 ]; 409 410 private bool isSupportedArchiveExt(in string path) 411 { 412 import std.algorithm : endsWith; 413 import std.uni : toLower; 414 415 const lpath = path.toLower; 416 return lpath.endsWith(".zip") || lpath.endsWith(".tar.gz") || lpath.endsWith(".tar"); 417 } 418 419 private ArchiveFormat archiveFormat(in string path) 420 { 421 import std.algorithm : endsWith; 422 423 if (path.endsWith(".zip")) return ArchiveFormat.zip; 424 if (path.endsWith(".tar.gz")) return ArchiveFormat.targz; 425 if (path.endsWith(".tar")) return ArchiveFormat.tar; 426 assert(false); 427 } 428 429 private string urlLastComp(in string url) 430 { 431 size_t ind = url.length - 1; 432 while (ind >= 0 && url[ind] != '/') { 433 ind--; 434 } 435 return url[ind+1 .. $]; 436 } 437 438 private string likelySrcDirName(in string archive) 439 { 440 import std.algorithm : endsWith; 441 import std.uni : toLower; 442 443 assert(isSupportedArchiveExt(archive)); 444 445 foreach(ext; supportedArchiveExts) { 446 if (archive.toLower.endsWith(ext)) { 447 return archive[0 .. $-ext.length]; 448 } 449 } 450 assert(false); 451 } 452 453 unittest 454 { 455 assert(likelySrcDirName("/path/archivename.tar.gz") == "/path/archivename"); 456 }