manifest.ha (9875B)
1 // License: MPL-2.0 2 // (c) 2021-2022 Alexey Yerin <yyp@disroot.org> 3 // (c) 2021 Bor Grošelj Simić <bor.groseljsimic@telemach.net> 4 // (c) 2021 Drew DeVault <sir@cmpwn.com> 5 // (c) 2021 Ember Sawady <ecs@d2evs.net> 6 // (c) 2021 Thomas Bracht Laumann Jespersen <t@laumann.xyz> 7 use bufio; 8 use bytes; 9 use encoding::hex; 10 use encoding::utf8; 11 use errors; 12 use fmt; 13 use fs; 14 use hare::ast; 15 use hare::unparse; 16 use io; 17 use os; 18 use path; 19 use strconv; 20 use strings; 21 use time; 22 use temp; 23 24 // The manifest file format is a series of line-oriented records. Lines starting 25 // with # are ignored. 26 // 27 // - "version" indicates the manifest format version, currently 2. 28 // - "input" is an input file, and its fields are the file hash, path, inode, 29 // and mtime as a Unix timestamp. 30 // - "module" is a version of a module, and includes the module hash and the set 31 // of input hashes which produce it. 32 // - "tags" is a list of tags associated with a module version 33 34 def VERSION: int = 2; 35 36 fn getinput(in: []input, hash: []u8) nullable *input = { 37 for (let i = 0z; i < len(in); i += 1) { 38 if (bytes::equal(in[i].hash, hash)) { 39 return &in[i]; 40 }; 41 }; 42 return null; 43 }; 44 45 // Loads the module manifest from the build cache for the given ident. The 46 // return value borrows the ident parameter. If the module is not found, an 47 // empty manifest is returned. 48 export fn manifest_load(ctx: *context, ident: ast::ident) (manifest | error) = { 49 let manifest = manifest { 50 ident = ident, 51 inputs = [], 52 versions = [], 53 }; 54 let ipath = identpath(manifest.ident); 55 defer free(ipath); 56 let cachedir = path::init(ctx.cache, ipath)!; 57 let cachedir = path::string(&cachedir); 58 59 let mpath = path::init(cachedir, "manifest")!; 60 let mpath = path::string(&mpath); 61 62 let truefile = match (fs::open(ctx.fs, mpath, fs::flag::RDONLY)) { 63 case errors::noentry => 64 return manifest; 65 case let err: fs::error => 66 return err; 67 case let file: io::handle => 68 yield file; 69 }; 70 defer io::close(truefile)!; 71 72 let inputs: []input = [], versions: []version = []; 73 74 let buf: [4096]u8 = [0...]; 75 let file = bufio::init(truefile, buf, []); 76 for (true) { 77 let line = match (bufio::scanline(&file)) { 78 case io::EOF => 79 break; 80 case let err: io::error => 81 return err; 82 case let line: []u8 => 83 yield line; 84 }; 85 defer free(line); 86 87 let line = match (strings::fromutf8(line)) { 88 case utf8::invalid => 89 // Treat an invalid manifest as empty 90 return manifest; 91 case let s: str => 92 yield s; 93 }; 94 95 if (strings::hasprefix(line, "#")) { 96 continue; 97 }; 98 99 let tok = strings::tokenize(line, " "); 100 let kind = match (strings::next_token(&tok)) { 101 case void => 102 continue; 103 case let s: str => 104 yield s; 105 }; 106 107 switch (kind) { 108 case "version" => 109 let ver = match (strings::next_token(&tok)) { 110 case void => 111 return manifest; 112 case let s: str => 113 yield s; 114 }; 115 match (strconv::stoi(ver)) { 116 case let v: int => 117 if (v != VERSION) { 118 return manifest; 119 }; 120 case => 121 return manifest; 122 }; 123 case "input" => 124 let hash = match (strings::next_token(&tok)) { 125 case void => 126 return manifest; 127 case let s: str => 128 yield s; 129 }, path = match (strings::next_token(&tok)) { 130 case void => 131 return manifest; 132 case let s: str => 133 yield s; 134 }, inode = match (strings::next_token(&tok)) { 135 case void => 136 return manifest; 137 case let s: str => 138 yield s; 139 }, mtime = match (strings::next_token(&tok)) { 140 case void => 141 return manifest; 142 case let s: str => 143 yield s; 144 }; 145 146 let hash = match (hex::decodestr(hash)) { 147 case let b: []u8 => 148 yield b; 149 case => 150 return manifest; 151 }; 152 let inode = match (strconv::stoz(inode)) { 153 case let z: size => 154 yield z; 155 case => 156 return manifest; 157 }; 158 let mtime = match (strconv::stoi64(mtime)) { 159 case let i: i64 => 160 yield time::from_unix(i); 161 case => 162 return manifest; 163 }; 164 165 let parsed = parsename(path); 166 let ftype = match (type_for_ext(path)) { 167 case void => 168 return manifest; 169 case let ft: filetype => 170 yield ft; 171 }; 172 173 append(inputs, input { 174 hash = hash, 175 path = strings::dup(path), 176 ft = ftype, 177 stat = fs::filestat { 178 mask = fs::stat_mask::MTIME | fs::stat_mask::INODE, 179 mtime = mtime, 180 inode = inode, 181 ... 182 }, 183 basename = strings::dup(parsed.0), 184 tags = parsed.2, 185 }); 186 case "module" => 187 let modhash = match (strings::next_token(&tok)) { 188 case void => 189 return manifest; 190 case let s: str => 191 yield s; 192 }; 193 let modhash = match (hex::decodestr(modhash)) { 194 case let b: []u8 => 195 yield b; 196 case => 197 return manifest; 198 }; 199 200 let minputs: []input = []; 201 for (true) { 202 let hash = match (strings::next_token(&tok)) { 203 case void => 204 break; 205 case let s: str => 206 yield s; 207 }; 208 let hash = match (hex::decodestr(hash)) { 209 case let b: []u8 => 210 yield b; 211 case => 212 return manifest; 213 }; 214 defer free(hash); 215 216 let input = match (getinput(inputs, hash)) { 217 case null => 218 return manifest; 219 case let i: *input => 220 yield i; 221 }; 222 append(minputs, *input); 223 }; 224 225 append(versions, version { 226 hash = modhash, 227 inputs = minputs, 228 ... 229 }); 230 case "tags" => 231 let modhash = match (strings::next_token(&tok)) { 232 case void => 233 return manifest; 234 case let s: str => 235 yield s; 236 }; 237 let modhash = match (hex::decodestr(modhash)) { 238 case let b: []u8 => 239 yield b; 240 case => 241 return manifest; 242 }; 243 244 const tags = strings::remaining_tokens(&tok); 245 const tags = parsetags(tags) as []tag; 246 let found = false; 247 for (let i = 0z; i < len(versions); i += 1) { 248 if (bytes::equal(versions[i].hash, modhash)) { 249 versions[i].tags = tags; 250 found = true; 251 break; 252 }; 253 }; 254 // Implementation detail: tags always follows module 255 // directive for a given module version 256 assert(found); 257 258 // Drain tokenizer 259 for (strings::next_token(&tok) is str) void; 260 case => 261 return manifest; 262 }; 263 264 // Check for extra tokens 265 match (strings::next_token(&tok)) { 266 case void => void; 267 case str => 268 return manifest; 269 }; 270 }; 271 272 manifest.inputs = inputs; 273 manifest.versions = versions; 274 return manifest; 275 }; 276 277 // Returns true if the desired module version is present and current in this 278 // manifest. 279 export fn current(man: *manifest, ver: *version) bool = { 280 // TODO: This is kind of dumb. What we really need to do is: 281 // 1. Update scan to avoid hashing the file if a manifest is present, 282 // and indicate that the hash is cached somewhere in the type. Get an 283 // up-to-date stat. 284 // 2. In [current], test if the inode and mtime are equal to the 285 // manifest version. If so, presume the file is up-to-date. If not, 286 // check the hash and update the manifest to the new inode/mtime if 287 // the hash matches. If not, the module is not current; rebuild. 288 let cached: nullable *version = null; 289 for (let i = 0z; i < len(man.versions); i += 1) { 290 if (bytes::equal(man.versions[i].hash, ver.hash)) { 291 cached = &man.versions[i]; 292 break; 293 }; 294 }; 295 let cached = match (cached) { 296 case null => 297 return false; 298 case let v: *version => 299 yield v; 300 }; 301 302 assert(len(cached.inputs) == len(ver.inputs)); 303 for (let i = 0z; i < len(cached.inputs); i += 1) { 304 let a = cached.inputs[i], b = cached.inputs[i]; 305 assert(a.path == b.path); 306 let ast = a.stat, bst = b.stat; 307 if (ast.inode != bst.inode 308 || time::compare(ast.mtime, bst.mtime) != 0) { 309 return false; 310 }; 311 }; 312 return true; 313 }; 314 315 // Writes a module manifest to the build cache. 316 export fn manifest_write(ctx: *context, man: *manifest) (void | error) = { 317 let ipath = identpath(man.ident); 318 defer free(ipath); 319 let cachedir = path::init(ctx.cache, ipath)!; 320 let cachedir = path::string(&cachedir); 321 322 let mpath = path::init(cachedir, "manifest")!; 323 let mpath = path::string(&mpath); 324 325 let (truefile, name) = temp::named(ctx.fs, cachedir, io::mode::WRITE, 0o644)?; 326 let wbuf: [os::BUFSIZ]u8 = [0...]; 327 let file = &bufio::init(truefile, [], wbuf); 328 defer { 329 bufio::flush(file)!; 330 fs::remove(ctx.fs, name): void; 331 io::close(truefile)!; 332 }; 333 334 let ident = unparse::identstr(man.ident); 335 defer free(ident); 336 fmt::fprintfln(file, "# {}", ident)?; 337 fmt::fprintln(file, "# This file is an internal Hare implementation detail.")?; 338 fmt::fprintln(file, "# The format is not stable.")?; 339 fmt::fprintfln(file, "version {}", VERSION)?; 340 for (let i = 0z; i < len(man.inputs); i += 1) { 341 const input = man.inputs[i]; 342 let hash = hex::encodestr(input.hash); 343 defer free(hash); 344 345 const want = fs::stat_mask::INODE | fs::stat_mask::MTIME; 346 assert(input.stat.mask & want == want); 347 fmt::fprintfln(file, "input {} {} {} {}", 348 hash, input.path, input.stat.inode, 349 time::unix(input.stat.mtime))?; 350 }; 351 352 for (let i = 0z; i < len(man.versions); i += 1) { 353 const ver = man.versions[i]; 354 let hash = hex::encodestr(ver.hash); 355 defer free(hash); 356 357 fmt::fprintf(file, "module {}", hash)?; 358 359 for (let j = 0z; j < len(ver.inputs); j += 1) { 360 let hash = hex::encodestr(ver.inputs[i].hash); 361 defer free(hash); 362 363 fmt::fprintf(file, " {}", hash)?; 364 }; 365 366 fmt::fprintln(file)?; 367 368 fmt::fprintf(file, "tags {} ", hash)?; 369 for (let i = 0z; i < len(ver.tags); i += 1) { 370 const tag = &ver.tags[i]; 371 fmt::fprintf(file, "{}{}", 372 switch (tag.mode) { 373 case tag_mode::INCLUSIVE => 374 yield "+"; 375 case tag_mode::EXCLUSIVE => 376 yield "-"; 377 }, 378 tag.name)?; 379 }; 380 fmt::fprintln(file)!; 381 }; 382 383 fs::move(ctx.fs, name, mpath)?; 384 }; 385 386 fn input_finish(in: *input) void = { 387 free(in.hash); 388 free(in.path); 389 free(in.basename); 390 tags_free(in.tags); 391 }; 392 393 // Frees resources associated with this manifest. 394 export fn manifest_finish(m: *manifest) void = { 395 for (let i = 0z; i < len(m.inputs); i += 1) { 396 input_finish(&m.inputs[i]); 397 }; 398 399 for (let i = 0z; i < len(m.versions); i += 1) { 400 free(m.versions[i].inputs); 401 tags_free(m.versions[i].tags); 402 }; 403 };