1 /** 2 * Copyright: Copyright Jason White, 2016 3 * License: MIT 4 * Authors: Jason White 5 */ 6 module button.resource; 7 import std.digest.digest : DigestType, isDigest; 8 9 import std.array : Appender; 10 11 /** 12 * A resource identifier. 13 */ 14 alias ResourceId = string; 15 16 /** 17 * A resource key must be unique. 18 */ 19 struct ResourceKey 20 { 21 /** 22 * File path to the resource. To ensure uniqueness, this should never be 23 * changed after construction. 24 */ 25 string path; 26 27 /** 28 * Compares this key with another. 29 */ 30 int opCmp()(const auto ref typeof(this) that) const pure nothrow 31 { 32 import std.algorithm.comparison : cmp; 33 return cmp(this.path, that.path); 34 } 35 } 36 37 unittest 38 { 39 static assert(ResourceKey("abc") == ResourceKey("abc")); 40 static assert(ResourceKey("abc") < ResourceKey("abcd")); 41 } 42 43 /** 44 * Compute the checksum of a file. 45 */ 46 private DigestType!Hash digestFile(Hash)(string path) 47 if (isDigest!Hash) 48 { 49 import std.digest.digest : digest; 50 import io.file : SysException, File, FileFlags; 51 import io.range : byChunk; 52 53 ubyte[4096] buf; 54 55 try 56 { 57 return digest!Hash(File(path, FileFlags.readExisting).byChunk(buf)); 58 } 59 catch (SysException e) 60 { 61 // This may fail if the given path is a directory. The path could have 62 // also been deleted. 63 return typeof(return).init; 64 } 65 } 66 67 /** 68 * A representation of a file on the disk. 69 * 70 * TODO: Support directories as well as files. 71 */ 72 struct Resource 73 { 74 import std.datetime : SysTime; 75 import std.digest.digest : DigestType; 76 import std.digest.sha : SHA256; 77 78 /** 79 * Digest to use to determine changes. 80 */ 81 alias Digest = SHA256; 82 83 enum Status 84 { 85 unknown = SysTime.max, 86 notFound = SysTime.min, 87 } 88 89 /** 90 * File path to the resource. To ensure uniqueness, this should never be 91 * changed after construction. 92 */ 93 ResourceId path; 94 95 /** 96 * Last time the file was modified. 97 */ 98 SysTime lastModified = Status.unknown; 99 100 /** 101 * Checksum of the file. 102 * 103 * TODO: If this is a directory, checksum the sorted list of its contents. 104 */ 105 DigestType!Digest checksum; 106 107 this(ResourceId path, SysTime lastModified = Status.unknown, 108 const(ubyte[]) checksum = []) pure 109 { 110 import std.algorithm.comparison : min; 111 112 this.path = path; 113 this.lastModified = lastModified; 114 115 // The only times the length will be different are: 116 // - The database is corrupt 117 // - The digest length changed 118 // In either case, it doesn't matter. If the checksum changes it will 119 // simply be recomputed and order will once again be restored in the 120 // realm. 121 immutable bytes = min(this.checksum.length, checksum.length); 122 this.checksum[0 .. bytes] = checksum[0 .. bytes]; 123 } 124 125 /** 126 * Returns a string representation of this resource. This is just the path 127 * to the resource. 128 */ 129 string toString() const pure nothrow 130 { 131 return path; 132 } 133 134 /** 135 * Returns a short string representation of the path. 136 */ 137 @property string toShortString() const pure nothrow 138 { 139 import std.path : baseName; 140 return path.baseName; 141 } 142 143 /** 144 * Returns the unique identifier for this vertex. 145 */ 146 @property inout(ResourceId) identifier() inout pure nothrow 147 { 148 return path; 149 } 150 151 /** 152 * Compares the file path of this resource with another. 153 */ 154 int opCmp()(const auto ref Resource rhs) const pure 155 { 156 import std.path : filenameCmp; 157 return filenameCmp(this.path, rhs.path); 158 } 159 160 /// Ditto 161 bool opEquals()(const auto ref Resource rhs) const pure 162 { 163 return opCmp(rhs) == 0; 164 } 165 166 unittest 167 { 168 assert(Resource("a") < Resource("b")); 169 assert(Resource("b") > Resource("a")); 170 171 assert(Resource("test", SysTime(1)) == Resource("test", SysTime(1))); 172 assert(Resource("test", SysTime(1)) == Resource("test", SysTime(2))); 173 } 174 175 /** 176 * Updates the last modified time and checksum of this resource. Returns 177 * true if anything changed. 178 * 179 * Note that the checksum is not recomputed if the modification time is the 180 * same. 181 */ 182 bool update() 183 { 184 import std.file : timeLastModified, FileException; 185 186 immutable lastModified = timeLastModified(path, Status.notFound); 187 188 if (lastModified != this.lastModified) 189 { 190 import std.digest.md; 191 this.lastModified = lastModified; 192 193 if (lastModified != Status.notFound) 194 { 195 auto checksum = digestFile!Digest(path); 196 if (checksum != this.checksum) 197 { 198 this.checksum = checksum; 199 return true; 200 } 201 202 // Checksum didn't change. 203 return false; 204 } 205 206 return true; 207 } 208 209 return false; 210 } 211 212 /** 213 * Returns true if the status of this resource is known. 214 */ 215 @property bool statusKnown() const pure nothrow 216 { 217 return lastModified != Status.unknown; 218 } 219 220 /** 221 * Deletes the resource from disk. 222 */ 223 void remove(bool dryRun) nothrow 224 { 225 import std.file : unlink = remove, isFile; 226 import io; 227 228 // Only delete this file if we know about it. This helps prevent the 229 // build system from haphazardly deleting files that were added to the 230 // build description but never output by a task. 231 if (!statusKnown) 232 return; 233 234 // TODO: Use rmdir instead if this is a directory. 235 236 if (!dryRun) 237 { 238 try 239 { 240 unlink(path); 241 } 242 catch (Exception e) 243 { 244 } 245 } 246 247 lastModified = Status.notFound; 248 } 249 } 250 251 /** 252 * Normalizes a resource path while trying to make it relative to the buildRoot. 253 * If it cannot be done, the path is made absolute. 254 * 255 * Params: 256 * buildRoot = The root directory of the build. Probably always the current 257 * working directory. 258 * taskDir = The working directory of the task this is for. The path is 259 * normalized relative to this directory. 260 * path = The path to be normalized. 261 */ 262 string normPath(const(char)[] buildRoot, const(char)[] taskDir, 263 const(char)[] path) pure 264 { 265 import std.path : isAbsolute, buildNormalizedPath, pathSplitter, 266 filenameCmp, dirSeparator; 267 import std.algorithm.searching : skipOver; 268 import std.algorithm.iteration : joiner; 269 import std.array : array; 270 import std.utf : byChar; 271 272 auto normalized = buildNormalizedPath(taskDir, path); 273 274 // If the normalized path is absolute, get a relative path if the absolute 275 // path is inside the working directory. This is done instead of always 276 // getting a relative path because we don't want to get relative paths to 277 // directories like "/usr/include". If the build directory moves, absolute 278 // paths outside will become invalid. 279 if (isAbsolute(normalized) && buildRoot.length) 280 { 281 auto normPS = pathSplitter(normalized); 282 auto buildPS = pathSplitter(buildRoot); 283 284 alias pred = (a, b) => filenameCmp(a, b) == 0; 285 286 if (skipOver!pred(normPS, &buildPS) && buildPS.empty) 287 return normPS.joiner(dirSeparator).byChar.array; 288 } 289 290 return normalized; 291 } 292 293 pure unittest 294 { 295 version (Posix) 296 { 297 assert(normPath("", "", "foo") == "foo"); 298 assert(normPath("", "foo", "bar") == "foo/bar"); 299 300 assert(normPath("", "foo/../foo/.", "bar/../baz") == "foo/baz"); 301 302 assert(normPath("", "foo", "/usr/include/bar") == "/usr/include/bar"); 303 assert(normPath("/usr", "foo", "/usr/bar") == "bar"); 304 assert(normPath("/usr/include", "foo", "/usr/bar") == "/usr/bar"); 305 } 306 } 307 308 /** 309 * Output range of implicit resources. 310 * 311 * This is used to easily accumulate implicit resources while also normalizing 312 * their paths at the same time. 313 */ 314 struct Resources 315 { 316 import std.array : Appender; 317 import std.range : isInputRange, ElementType; 318 319 Appender!(Resource[]) resources; 320 321 alias resources this; 322 323 string buildDir; 324 string taskDir; 325 326 this(string buildDir, string taskDir) 327 { 328 this.buildDir = buildDir; 329 this.taskDir = taskDir; 330 } 331 332 void put(R)(R items) 333 if (isInputRange!R && is(ElementType!R : const(char)[])) 334 { 335 import std.range : empty, popFront, front; 336 337 for (; !items.empty; items.popFront()) 338 put(items.front); 339 } 340 341 void put(const(char)[] item) 342 { 343 resources.put(Resource(normPath(buildDir, taskDir, item))); 344 } 345 346 void put(R)(R items) 347 if (isInputRange!R && is(ElementType!R : Resource)) 348 { 349 resources.put(items); 350 } 351 352 void put(Resource item) 353 { 354 item.path = normPath(buildDir, taskDir, item.path); 355 resources.put(item); 356 } 357 }