hare

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

scan.ha (11454B)


      1 // License: MPL-2.0
      2 // (c) 2021-2022 Alexey Yerin <yyp@disroot.org>
      3 // (c) 2022 Bor Grošelj Simić <bor.groseljsimic@telemach.net>
      4 // (c) 2021-2022 Drew DeVault <sir@cmpwn.com>
      5 // (c) 2021 Eyal Sawady <ecs@d2evs.net>
      6 // (c) 2021 Kiëd Llaentenn <kiedtl@tilde.team>
      7 // (c) 2021 Thomas Bracht Laumann Jespersen <t@laumann.xyz>
      8 use ascii;
      9 use crypto::sha256;
     10 use fs;
     11 use hare::ast;
     12 use hare::lex;
     13 use hare::parse;
     14 use hash;
     15 use io;
     16 use path;
     17 use sort;
     18 use strings;
     19 use strio;
     20 
     21 def ABI_VERSION: u8 = 4;
     22 
     23 // Scans the files in a directory for eligible build inputs and returns a
     24 // [[version]] which includes all applicable files and their dependencies.
     25 export fn scan(ctx: *context, path: str) (version | error) = {
     26 	// TODO: Incorporate defines into the hash
     27 	let sha = sha256::sha256();
     28 	let found = false;
     29 	for (let i = 0z; i < len(ctx.tags); i += 1) {
     30 		if (ctx.tags[i].mode == tag_mode::INCLUSIVE
     31 				&& ctx.tags[i].name == "test") {
     32 			found = true;
     33 			break;
     34 		};
     35 	};
     36 	hash::write(&sha, [if (found) 1 else 0]);
     37 	let iter = match (fs::iter(ctx.fs, path)) {
     38 	case fs::wrongtype =>
     39 		// Single file case
     40 		let inputs: []input = [];
     41 		let deps: []ast::ident = [];
     42 		let ft = match (type_for_ext(path)) {
     43 		case void =>
     44 			return notfound;
     45 		case let ft: filetype =>
     46 			yield ft;
     47 		};
     48 		let path = fs::resolve(ctx.fs, path);
     49 		let st = fs::stat(ctx.fs, path)?;
     50 		let in = input {
     51 			path = strings::dup(path),
     52 			stat = st,
     53 			ft = ft,
     54 			hash = scan_file(ctx, path, ft, &deps)?,
     55 			...
     56 		};
     57 		append(inputs, in);
     58 
     59 		let sumbuf: [sha256::SIZE]u8 = [0...];
     60 		hash::write(&sha, in.hash);
     61 		hash::sum(&sha, sumbuf);
     62 
     63 		return version {
     64 			hash = sumbuf,
     65 			basedir = strings::dup(path::dirname(path)),
     66 			depends = deps,
     67 			inputs = inputs,
     68 			...
     69 		};
     70 	case let err: fs::error =>
     71 		return err;
     72 	case let iter: *fs::iterator =>
     73 		yield iter;
     74 	};
     75 	defer fs::finish(iter);
     76 	let ver = version {
     77 		basedir = strings::dup(path),
     78 		...
     79 	};
     80 	scan_directory(ctx, &ver, &sha, path, iter)?;
     81 
     82 	let readme = path::join(path, "README");
     83 	defer free(readme);
     84 	if (len(ver.inputs) == 0 && !fs::exists(ctx.fs, readme)) {
     85 		// TODO: HACK: README is a workaround for haredoc issues
     86 		return notfound;
     87 	};
     88 
     89 	let tmp: [sha256::SIZE]u8 = [0...];
     90 	hash::sum(&sha, tmp);
     91 	ver.hash = alloc([], sha.sz);
     92 	append(ver.hash, tmp...);
     93 	return ver;
     94 };
     95 
     96 // Given a file or directory name, parses it into the basename, extension, and
     97 // tag set.
     98 export fn parsename(name: str) (str, str, []tag) = {
     99 	let ext = path::extension(name);
    100 	let base = ext.0, ext = ext.1;
    101 
    102 	let p = strings::index(base, '+');
    103 	let m = strings::index(base, '-');
    104 	if (p is void && m is void) {
    105 		return (base, ext, []);
    106 	};
    107 	let i: size =
    108 		if (p is void && m is size) m: size
    109 		else if (m is void && p is size) p: size
    110 		else if (m: size < p: size) m: size
    111 		else p: size;
    112 	let tags = strings::sub(base, i, strings::end);
    113 	let tags = match (parsetags(tags)) {
    114 	case void =>
    115 		return (base, ext, []);
    116 	case let t: []tag =>
    117 		yield t;
    118 	};
    119 	let base = strings::sub(base, 0, i);
    120 	return (base, ext, tags);
    121 };
    122 
    123 fn scan_directory(
    124 	ctx: *context,
    125 	ver: *version,
    126 	sha: *hash::hash,
    127 	path: str,
    128 	iter: *fs::iterator,
    129 ) (void | error) = {
    130 	let files: []str = [], dirs: []str = [];
    131 	defer {
    132 		strings::freeall(files);
    133 		strings::freeall(dirs);
    134 	};
    135 
    136 	let pathbuf = path::init();
    137 	for (true) {
    138 		const ent = match (fs::next(iter)) {
    139 		case void =>
    140 			break;
    141 		case let ent: fs::dirent =>
    142 			yield ent;
    143 		};
    144 
    145 		switch (ent.ftype) {
    146 		case fs::mode::LINK =>
    147 			let linkpath = path::set(&pathbuf, path, ent.name)!;
    148 			linkpath = fs::readlink(ctx.fs, linkpath)?;
    149 			if (!path::abs(linkpath)) {
    150 				linkpath = path::set(&pathbuf, path, linkpath)!;
    151 			};
    152 
    153 			const st = fs::stat(ctx.fs, linkpath)?;
    154 			if (fs::isfile(st.mode)) {
    155 				append(files, strings::dup(ent.name));
    156 			} else if (fs::isdir(st.mode)) {
    157 				append(dirs, strings::dup(ent.name));
    158 			} else if (fs::islink(st.mode)) {
    159 				abort(); // TODO: Resolve recursively
    160 			};
    161 		case fs::mode::DIR =>
    162 			append(dirs, strings::dup(ent.name));
    163 		case fs::mode::REG =>
    164 			append(files, strings::dup(ent.name));
    165 		case => void;
    166 		};
    167 	};
    168 
    169 	// Sorted to keep the hash consistent
    170 	sort::strings(dirs);
    171 	sort::strings(files);
    172 
    173 	// Tuple of is_directory, basename, tags, and path to a candidate input.
    174 	let inputs: [](bool, str, []tag, str) = [];
    175 	defer for (let i = 0z; i < len(inputs); i += 1) {
    176 		// For file paths, these are assigned to the input, which
    177 		// assumes ownership over them.
    178 		if (inputs[i].0) {
    179 			free(inputs[i].1);
    180 			tags_free(inputs[i].2);
    181 			free(inputs[i].3);
    182 		};
    183 	};
    184 
    185 	// For a given basename, only the most specific path (i.e. with the most
    186 	// tags) is used.
    187 	//
    188 	// foo.ha
    189 	// foo+linux.ha
    190 	// foo+linux+x86_64/
    191 	// 	bar.ha
    192 	// 	baz.ha
    193 	//
    194 	// In this case, foo+linux+x86_64 is the most specific, and so its used
    195 	// as the build input and the other two files are discarded.
    196 
    197 	for (let i = 0z; i < len(dirs); i += 1) {
    198 		let name = dirs[i];
    199 		let parsed = parsename(name);
    200 		let base = parsed.0, tags = parsed.2;
    201 
    202 		if (!strings::hasprefix(name, "+")
    203 				&& !strings::hasprefix(name, "-")) {
    204 			if (!strings::hasprefix(name, ".")) {
    205 				append(ver.subdirs, strings::dup(name));
    206 			};
    207 			continue;
    208 		};
    209 		if (!tagcompat(ctx.tags, tags)) {
    210 			continue;
    211 		};
    212 
    213 		let path = path::join(path, name);
    214 		let tuple = (true, strings::dup(base), tags, path);
    215 		let superceded = false;
    216 		for (let j = 0z; j < len(inputs); j += 1) {
    217 			if (inputs[j].1 != base) {
    218 				continue;
    219 			};
    220 			let theirs = inputs[j].2;
    221 			if (len(theirs) < len(tags)) {
    222 				free(inputs[j].1);
    223 				tags_free(inputs[j].2);
    224 				free(inputs[j].3);
    225 				inputs[j] = tuple;
    226 				superceded = true;
    227 				break;
    228 			} else if (len(theirs) > len(tags)) {
    229 				// They are more specific
    230 				superceded = true;
    231 				break;
    232 			} else if (len(base) != 0) {
    233 				return (path, inputs[j].3): ambiguous;
    234 			};
    235 		};
    236 		if (!superceded) {
    237 			append(inputs, tuple);
    238 		};
    239 	};
    240 
    241 	for (let i = 0z; i < len(files); i += 1) {
    242 		let name = files[i];
    243 		let parsed = parsename(name);
    244 		let base = parsed.0, ext = parsed.1, tags = parsed.2;
    245 
    246 		let eligible = false;
    247 		static const exts = [".ha", ".s"];
    248 		for (let i = 0z; i < len(exts); i += 1) {
    249 			if (exts[i] == ext) {
    250 				eligible = true;
    251 				break;
    252 			};
    253 		};
    254 		if (!eligible || !tagcompat(ctx.tags, tags)) {
    255 			tags_free(tags);
    256 			continue;
    257 		};
    258 
    259 		let path = path::join(path, name);
    260 		let tuple = (false, strings::dup(base), tags, path);
    261 		let superceded = false;
    262 		for (let j = 0z; j < len(inputs); j += 1) {
    263 			if (inputs[j].1 != base) {
    264 				continue;
    265 			};
    266 			let theirs = inputs[j].2;
    267 			if (len(theirs) < len(tags)) {
    268 				// We are more specific
    269 				free(inputs[j].1);
    270 				tags_free(inputs[j].2);
    271 				free(inputs[j].3);
    272 				inputs[j] = tuple;
    273 				superceded = true;
    274 				break;
    275 			} else if (len(theirs) > len(tags)) {
    276 				// They are more specific
    277 				superceded = true;
    278 				break;
    279 			} else if (len(base) != 0) {
    280 				return (path, inputs[j].3): ambiguous;
    281 			};
    282 		};
    283 		if (!superceded) {
    284 			append(inputs, tuple);
    285 		};
    286 	};
    287 
    288 	for (let i = 0z; i < len(inputs); i += 1) {
    289 		let isdir = inputs[i].0, path = inputs[i].3;
    290 		if (isdir) {
    291 			let iter = fs::iter(ctx.fs, path)?;
    292 			defer fs::finish(iter);
    293 			scan_directory(ctx, ver, sha, path, iter)?;
    294 		} else {
    295 			let path = fs::resolve(ctx.fs, path);
    296 			let st = fs::stat(ctx.fs, path)?;
    297 			let ftype = type_for_ext(path) as filetype;
    298 			let in = input {
    299 				path = strings::dup(path),
    300 				stat = st,
    301 				ft = ftype,
    302 				hash = scan_file(ctx, path, ftype, &ver.depends)?,
    303 				basename = inputs[i].1,
    304 				tags = inputs[i].2,
    305 				...
    306 			};
    307 			append(ver.inputs, in);
    308 			hash::write(sha, in.hash);
    309 		};
    310 	};
    311 };
    312 
    313 // Looks up a module by its identifier from HAREPATH, and returns a [[version]]
    314 // which includes all eligible build inputs.
    315 export fn lookup(ctx: *context, name: ast::ident) (version | error) = {
    316 	let ipath = identpath(name);
    317 	defer free(ipath);
    318 	for (let i = len(ctx.paths); i > 0; i -= 1) {
    319 		let cand = path::join(ctx.paths[i - 1], ipath);
    320 		defer free(cand);
    321 		match (scan(ctx, cand)) {
    322 		case let v: version =>
    323 			return v;
    324 		case error => void;
    325 		};
    326 	};
    327 	return notfound;
    328 };
    329 
    330 fn type_for_ext(name: str) (filetype | void) = {
    331 	const ext = path::extension(name).1;
    332 	return
    333 		if (ext == ".ha") filetype::HARE
    334 		else if (ext == ".s") filetype::ASSEMBLY
    335 		else void;
    336 };
    337 
    338 fn scan_file(
    339 	ctx: *context,
    340 	path: str,
    341 	ftype: filetype,
    342 	deps: *[]ast::ident,
    343 ) ([]u8 | error) = {
    344 	let f = fs::open(ctx.fs, path)?;
    345 	let sha = sha256::sha256();
    346 	hash::write(&sha, strings::toutf8(path));
    347 	hash::write(&sha, [ABI_VERSION]);
    348 
    349 	if (ftype == filetype::HARE) {
    350 		let tee = io::tee(f, &sha);
    351 		let lexer = lex::init(&tee, path);
    352 		let imports = match (parse::imports(&lexer)) {
    353 		case let im: []ast::import =>
    354 			yield im;
    355 		case let err: parse::error =>
    356 			io::close(f): void;
    357 			return err;
    358 		};
    359 		for (let i = 0z; i < len(imports); i += 1) {
    360 			if (!have_ident(deps, imports[i].ident)) {
    361 				append(deps, imports[i].ident);
    362 			};
    363 		};
    364 		// Finish spooling out the file for the SHA
    365 		match (io::copy(io::empty, &tee)) {
    366 		case size => void;
    367 		case let err: io::error =>
    368 			io::close(f): void;
    369 			return err;
    370 		};
    371 	} else {
    372 		match (io::copy(&sha, f)) {
    373 		case size => void;
    374 		case let err: io::error =>
    375 			io::close(f): void;
    376 			return err;
    377 		};
    378 	};
    379 	io::close(f)?;
    380 
    381 	let tmp: [sha256::SIZE]u8 = [0...];
    382 	hash::sum(&sha, tmp);
    383 
    384 	let checksum: []u8 = alloc([], sha.sz);
    385 	append(checksum, tmp...);
    386 	return checksum;
    387 };
    388 
    389 fn have_ident(sl: *[]ast::ident, id: ast::ident) bool = {
    390 	for (let i = 0z; i < len(sl); i += 1) {
    391 		if (ast::ident_eq(sl[i], id)) {
    392 			return true;
    393 		};
    394 	};
    395 	return false;
    396 };
    397 
    398 // Parses a set of build tags, returning void if the string is an invalid tag
    399 // set. The caller must free the return value with [[tags_free]].
    400 export fn parsetags(in: str) ([]tag | void) = {
    401 	let tags: []tag = [];
    402 	let iter = strings::iter(in);
    403 	for (true) {
    404 		let t = tag { ... };
    405 		let m = match (strings::next(&iter)) {
    406 		case void =>
    407 			break;
    408 		case let r: rune =>
    409 			yield r;
    410 		};
    411 		t.mode = switch (m) {
    412 		case =>
    413 			tags_free(tags);
    414 			return;
    415 		case '+' =>
    416 			yield tag_mode::INCLUSIVE;
    417 		case '-' =>
    418 			yield tag_mode::EXCLUSIVE;
    419 		};
    420 		let buf = strio::dynamic();
    421 		for (true) match (strings::next(&iter)) {
    422 		case void =>
    423 			break;
    424 		case let r: rune =>
    425 			if (ascii::isalnum(r) || r == '_') {
    426 				strio::appendrune(&buf, r)!;
    427 			} else {
    428 				strings::push(&iter, r);
    429 				break;
    430 			};
    431 		};
    432 		t.name = strio::string(&buf);
    433 		append(tags, t);
    434 	};
    435 	return tags;
    436 };
    437 
    438 // Frees a set of tags.
    439 export fn tags_free(tags: []tag) void = {
    440 	for (let i = 0z; i < len(tags); i += 1) {
    441 		free(tags[i].name);
    442 	};
    443 	free(tags);
    444 };
    445 
    446 // Duplicates a set of tags.
    447 export fn tags_dup(tags: []tag) []tag = {
    448 	let new: []tag = alloc([], len(tags));
    449 	for (let i = 0z; i < len(tags); i += 1) {
    450 		append(new, tag {
    451 			name = strings::dup(tags[i].name),
    452 			mode = tags[i].mode,
    453 		});
    454 	};
    455 	return new;
    456 };
    457 
    458 // Compares two tag sets and tells you if they are compatible.
    459 export fn tagcompat(have: []tag, want: []tag) bool = {
    460 	// XXX: O(n²), lame
    461 	for (let i = 0z; i < len(want); i += 1) {
    462 		let present = false;
    463 		for (let j = 0z; j < len(have); j += 1) {
    464 			if (have[j].name == want[i].name) {
    465 				present = have[j].mode == tag_mode::INCLUSIVE;
    466 				break;
    467 			};
    468 		};
    469 		switch (want[i].mode) {
    470 		case tag_mode::INCLUSIVE =>
    471 			if (!present) return false;
    472 		case tag_mode::EXCLUSIVE =>
    473 			if (present) return false;
    474 		};
    475 	};
    476 	return true;
    477 };