Skip to main content
Glama
makefile.bzl6.07 kB
# Copyright (c) Meta Platforms, Inc. and affiliates. # # This source code is licensed under both the MIT license found in the # LICENSE-MIT file in the root directory of this source tree and the Apache # License, Version 2.0 found in the LICENSE-APACHE file in the root directory # of this source tree. # Parse and resolve Makefile's produce by ocamldeps. load("@prelude//:paths.bzl", "paths") # [Note: Dynamic dependency calculations] # --------------------------------------- # We are given the contents of a Makefile produced by ocamldep in -native mode e.g. # ``` # quux.cmi: # quux.cmx: # quux.cmi # corge.cmx: # quux.cmx #``` # and the sources we care about. # #'parse_makefile' produces a mapping of which sources depend on which. In fact, # it computes a pair of mappings: # # Result (1): Replace '.cmi' with '.mli' and '.cmx' with '.ml' e.g. # ``` # quux.mli: # quux.ml: # quux.mli # corge.ml: # quux.ml # ``` # If -opqaue is not in effect then this reflects the dependency requirements # that enable cross module inlining (see the description of the -opaque flag [in # the ocamlopt manual](https://v2.ocaml.org/manual/native.html)). # Result(2): # # If '-opqaue' is not in effect, then this is a copy of result(1). If though # '-opaque' is in effect, replace 'foo.cmx' with 'foo.cmi' in the RHS of rules # as long as there is a rule with LHS 'foo.cmi' in the ocamldep input. Then, # replace '.cmi' with .mli' and '.cmx' with '.ml' as before e.g. # ``` # quux.mli: # quux.ml: # quux.mli # corge.ml: # quux.mli # ``` # This mapping reflects the dependency requirements that don't admit cross # module inlining (cutting down significantly on recompilations when only a # module implementation and not its interface is changed). def parse_makefile(makefile: str, srcs: list[Artifact], opaque_enabled: bool) -> (dict[Artifact, list[Artifact]], dict[Artifact, list[Artifact]]): # ocamldep is always invoked with '-native' so compiled modules are always # reported as '.cmx' files contents = _parse(makefile) # Collect all the files we reference. files = {} for aa, bb in contents: for x in aa + bb: files[x] = None # Produce a mapping of the files on the LHS of rules to their source # equivalents e.g. {'corge.cmx': 'corge.ml', 'quux.cmx': 'quux.ml', # 'quux.mli': 'quux.mli'}. resolved = _resolve(files.keys(), srcs) # This nested loop is technically O(n^2), but since `a` is at most 2 in OCaml # it's not an issue result = {} for aa, bb in contents: for a in aa: if a in resolved: result[resolved[a]] = [resolved[b] for b in bb if b in resolved] # This is not an optimization, we rely on this early return (see [Note: # Dynamic dependencies] earlier in this file). if not opaque_enabled: return (result, result) # Replace '.cmx' (or '.cmo') dependencies with '.cmi' on the RHS of # rules everywhere we can. contents = [_replace_rhs_cmx_with_cmi(contents, c) for c in contents] # Compute a new result based on having replaced '.cmx' ('.cmo') with '.cmi' # on the RHS of rules where we can. result2 = {} for aa, bb in contents: for a in aa: if a in resolved: result2[resolved[a]] = [resolved[b] for b in bb if b in resolved] return (result, result2) def _resolve(entries: list[str], srcs: list[Artifact]) -> dict[str, Artifact]: # O(n^2), alas - switch to a prefix map if that turns out to be too slow. result = {} for x in entries: prefix, ext = paths.split_extension(x) if ext == ".cmx": y = prefix + ".ml" elif ext == ".cmi": y = prefix + ".mli" else: fail("ocamldeps makefile, unexpected extension: " + x) possible = [src for src in srcs if y.endswith("/" + src.short_path)] if len(possible) == 1: result[x] = possible[0] elif len(possible) == 0: # If we get here, then ocamldep has found a dependency on a .ml file # that wasn't in `srcs`. On Remote Execution, that shouldn't happen, # and would be user error. When running locally, ocamldep will look # at any .ml files that are in the same directory as a `srcs` (no # way to stop it), so sometimes it finds those when they are meant # to be opaque or coming in through `deps`. The only solution is # just to ignore such unknown entries and "pretend" the .ml file # wasn't found. pass else: fail("ocamldeps makefile, multiple sources: " + x) return result # Do the low level parse, returning the strings that are in the makefile as # dependencies def _parse(makefile: str) -> list[(list[str], list[str])]: # For the OCaml makefile, it's a series of lines, often with a '\' line # continuation. lines = [] line = "" for x in makefile.splitlines(): if x.endswith("\\"): line += x[:-1] else: lines.append(line + x) line = "" lines.append(line) result = [] for x in lines: before, _, after = (x + " ").partition(" : ") result.append((before.split(), after.strip().split())) return result # If 'f.cmi' appears on the LHS of a rule, change occurrences of 'f.cmx' to # 'f.cmi' in all RHS's in the results of a parsed makefile. def _replace_rhs_cmx_with_cmi(contents: list[(list[str], list[str])], p: (list[str], list[str])) -> (list[str], list[str]): def replace_in(p): pre, ext = paths.split_extension(p) # Search the LHS's of contents for a for 'p.cmi'. exists = len([p for p in contents if pre + ".cmi" in p[0]]) != 0 if exists and ext == ".cmx": # Replace 'p.cmx' in this RHS with 'p.cmi'. return pre + ".cmi" return p xs, ys = p return (xs, [replace_in(y) for y in ys])

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/systeminit/si'

If you have feedback or need assistance with the MCP directory API, please join our Discord server