hare

[hare] The Hare programming language
git clone https://git.torresjrjr.com/hare.git
Log | Files | Refs | README | LICENSE

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 };