A dev session that ends cleanly takes its directories with it, and Build.executable says where the IR it kept is
This commit is contained in:
parent
37ccca56a3
commit
54ec52fde6
22
TODO.org
22
TODO.org
@ -1465,11 +1465,14 @@ in a merged build that process is the program, the compiler and the listener at
|
||||
once. It reported as a connection refusal on a path that plainly existed, which
|
||||
misdirected two investigations.
|
||||
|
||||
** TODO The daemon leaves its temp directory behind
|
||||
About seven megabytes a session, and nothing removes it. Two deliberate non-goals
|
||||
when it is fixed: not on a crash, because the directory is the post-mortem, and
|
||||
never another session's directory, because a stale pid is not proof of anything.
|
||||
The agent socket goes with it.
|
||||
** DONE The daemon leaves its temp directory behind
|
||||
CLOSED: [2026-09-25]
|
||||
A session that ends cleanly — =close=, or the editor gone past the grace —
|
||||
removes =flan-dev-<pid>= (program, modules, agent socket) and its own
|
||||
=Build.workdir=. Kept on a crash: the accept loop raising, or a two-process
|
||||
child killed by a signal. Only the two paths named for this pid are touched;
|
||||
nothing sweeps other sessions' directories. =flan build= and =flan run= still
|
||||
leave an empty =flan-<pid>= each; that is not this entry.
|
||||
|
||||
** DONE (agent/start) takes no argument, and binds before main
|
||||
CLOSED: [2026-09-20]
|
||||
@ -2010,9 +2013,12 @@ go through one helper. The CLI loads through its own path and checks with a
|
||||
different entry point, so this is not a matter of calling the test module from the
|
||||
binary — closing it means the pipeline moving into the library.
|
||||
|
||||
** TODO Build.executable returns only its output path
|
||||
The daemon recovers the host's IR file by recomputing the working directory. One
|
||||
line away: return the path rather than recomputing it.
|
||||
** DONE Build.executable returns only its output path
|
||||
CLOSED: [2026-09-25]
|
||||
It returns the output path and, under =keep=, the path of the IR or assembly
|
||||
it kept; without =keep= that file is gone and the second half is =None=. The
|
||||
two-process daemon moves the host's IR from the path it is given and no longer
|
||||
recomputes =Build.workdir=.
|
||||
|
||||
** TODO The 2MB OFL font is not vendored
|
||||
One example wants a font that is OFL and redistributable; it says on screen when
|
||||
|
||||
15
lib/build.ml
15
lib/build.ml
@ -729,7 +729,11 @@ let compile_c ~opts ?tflags ?(warn = []) ~src ~name () =
|
||||
|
||||
(* [csrcs] and [lflags] come from the imported packages (see [Load]): the C
|
||||
shim a package binds through, and the arguments needed to link the library
|
||||
it binds to. *)
|
||||
it binds to.
|
||||
|
||||
Returns the output path and, when [opts.keep] asked for it, the path of the
|
||||
IR (or, under [x86], the assembly) the build was made from. Without [keep]
|
||||
that file is removed before this returns, so there is no path to give. *)
|
||||
let executable ?(opts = default) ?(csrcs = []) ?(lflags = []) ?(pnames = [])
|
||||
(p : Tast.program) ~out =
|
||||
(* The JS dialect leaves here, before anything that assumes a clang. Its
|
||||
@ -749,7 +753,7 @@ let executable ?(opts = default) ?(csrcs = []) ?(lflags = []) ?(pnames = [])
|
||||
if opts.x86 then
|
||||
failwith "js: --x86 and --target=js are two different backends — pick one";
|
||||
write out (Js.program ~checks:opts.checks p);
|
||||
out
|
||||
(out, None)
|
||||
end
|
||||
else
|
||||
(* A dev build is the REPL's, and the REPL reaches a running process through
|
||||
@ -943,8 +947,11 @@ let executable ?(opts = default) ?(csrcs = []) ?(lflags = []) ?(pnames = [])
|
||||
let code = Sys.command cmd in
|
||||
if code <> 0 then
|
||||
failwith (Printf.sprintf "%s failed (exit %d); the IR is at %s" clang code ll);
|
||||
if not opts.keep then (try Sys.remove ll with Sys_error _ -> ());
|
||||
out
|
||||
if opts.keep then (out, Some ll)
|
||||
else begin
|
||||
(try Sys.remove ll with Sys_error _ -> ());
|
||||
(out, None)
|
||||
end
|
||||
|
||||
(* ── The dev path: one function into a loadable object ──────────────── *)
|
||||
|
||||
|
||||
79
lib/dev.ml
79
lib/dev.ml
@ -64,6 +64,10 @@ type t = {
|
||||
again ([eval]) and when a re-run is accepted ([rerun]), so the next park
|
||||
is a new one and gets the whole sentence. *)
|
||||
mutable park_noted : bool;
|
||||
(* How the two-process daemon's child ended, once [liveness] has reaped it.
|
||||
A signal here is a crash, and a crash keeps [dir] on disk: see
|
||||
[remove_session_dirs]. *)
|
||||
mutable died : Unix.process_status option;
|
||||
}
|
||||
|
||||
(* The program's stdout is a pipe into this process, so that an editor can see
|
||||
@ -597,7 +601,7 @@ let liveness t =
|
||||
Some
|
||||
(match Unix.waitpid [ Unix.WNOHANG ] child with
|
||||
| 0, _ -> true
|
||||
| _ -> false
|
||||
| _, status -> t.died <- Some status; false
|
||||
| exception Unix.Unix_error _ -> false)
|
||||
in
|
||||
liveness_of ~child_alive ~finished:t.finished ~program:(Program.state ())
|
||||
@ -4241,6 +4245,29 @@ let accept_loop ?grace t ls =
|
||||
in
|
||||
go ()
|
||||
|
||||
(* A session that ended cleanly takes its directories with it: [t.dir], which
|
||||
holds the program, every module it was sent and the agent's socket, and
|
||||
[Build.workdir], which holds what the builds left there. Both are named for
|
||||
this process's pid, so both are this session's and no other's. Nothing looks
|
||||
further than those two paths — a stale pid in another directory's name is
|
||||
not proof that its session is over.
|
||||
|
||||
Called only on the way out of a clean end. A crash leaves both where they
|
||||
are, because they are what there is to look at afterwards: the IR the
|
||||
program was built from and the modules it was running. *)
|
||||
let remove_session_dirs t =
|
||||
let rec remove path =
|
||||
match (Unix.lstat path).Unix.st_kind with
|
||||
| Unix.S_DIR ->
|
||||
Array.iter (fun n -> remove (Filename.concat path n))
|
||||
(try Sys.readdir path with Sys_error _ -> [||]);
|
||||
(try Unix.rmdir path with Unix.Unix_error _ -> ())
|
||||
| _ -> (try Unix.unlink path with Unix.Unix_error _ -> ())
|
||||
| exception Unix.Unix_error _ -> ()
|
||||
in
|
||||
remove t.dir;
|
||||
remove (Build.workdir ())
|
||||
|
||||
(* [debug] is off by default, which keeps [flan dev] exactly what it was: a
|
||||
-O2 host and -O2 modules. It is opt-in rather than always-on because a debug
|
||||
build is an -O0 build — [llvm.dbg.declare] describes an alloca and mem2reg
|
||||
@ -4266,28 +4293,26 @@ let two_process ?(debug = false) ?(x86 = true) ~file ~sock () =
|
||||
actually given, not a second emission of it, which is the difference
|
||||
between showing what the process was built from and showing what it
|
||||
probably was. [Build.executable] leaves it in its own working directory
|
||||
under the module's basename; it is moved here so that nothing else in this
|
||||
process can reuse the name. *)
|
||||
and says where; it is moved here so that nothing else in this process can
|
||||
reuse the name. *)
|
||||
(* The host and the modules are one decision. DWARF in a redefinition is
|
||||
only half a debuggable dev loop: lldb re-resolves a *name* breakpoint
|
||||
against each module as it loads either way, but a breakpoint set on a line
|
||||
in the .flan buffer needs a line table on both sides — the host's to fire
|
||||
before the first C-c C-c, the module's to follow the reload. *)
|
||||
ignore
|
||||
(Build.executable
|
||||
~opts:{ Build.default with Build.dev = true; Build.keep = true;
|
||||
Build.debug; Build.x86 }
|
||||
~csrcs:l.Load.csrcs ~lflags:l.Load.lflags session.Session.host ~out:exe);
|
||||
let _, kept =
|
||||
Build.executable
|
||||
~opts:{ Build.default with Build.dev = true; Build.keep = true;
|
||||
Build.debug; Build.x86 }
|
||||
~csrcs:l.Load.csrcs ~lflags:l.Load.lflags session.Session.host ~out:exe
|
||||
in
|
||||
(* Host and modules are chosen together, which is the whole licence: an
|
||||
[--x86] host gets [--x86] modules because one flag set both, and the
|
||||
source [Build.executable] kept is assembly rather than IR. *)
|
||||
let host_ll = Filename.concat dir (if x86 then "host.s" else "host.ll") in
|
||||
(try
|
||||
Sys.rename
|
||||
(Filename.concat (Build.workdir ())
|
||||
(Filename.basename exe ^ if x86 then ".s" else ".ll"))
|
||||
host_ll
|
||||
with Sys_error _ -> ());
|
||||
(match kept with
|
||||
| Some src -> (try Sys.rename src host_ll with Sys_error _ -> ())
|
||||
| None -> ());
|
||||
let agent = Filename.concat dir "agent.sock" in
|
||||
|
||||
(* The program's source names some socket path; the daemon is the one that
|
||||
@ -4337,7 +4362,7 @@ let two_process ?(debug = false) ?(x86 = true) ~file ~sock () =
|
||||
{ session; child = Some child; agent; dir; stdout = rd;
|
||||
out = Buffer.create 4096; n = 0; gen = 0; owners = Hashtbl.create 32;
|
||||
host_ll; host_exe = exe; finished = false; agent_watch = None;
|
||||
park_noted = false }
|
||||
park_noted = false; died = None }
|
||||
in
|
||||
ignore_sigpipe ();
|
||||
(try Unix.unlink sock with Unix.Unix_error _ -> ());
|
||||
@ -4352,7 +4377,13 @@ let two_process ?(debug = false) ?(x86 = true) ~file ~sock () =
|
||||
(try Unix.close ls with Unix.Unix_error _ -> ());
|
||||
(try Unix.close rd with Unix.Unix_error _ -> ());
|
||||
(try Unix.unlink sock with Unix.Unix_error _ -> ()))
|
||||
(fun () -> accept_loop t ls)
|
||||
(fun () -> accept_loop t ls);
|
||||
(* Here only when the loop returned: an exception out of it has already
|
||||
left through the [finally]. A child killed by a signal is the crash that
|
||||
keeps the directory. *)
|
||||
match t.died with
|
||||
| Some (Unix.WSIGNALED _) -> ()
|
||||
| _ -> remove_session_dirs t
|
||||
|
||||
(* ── One process: the program and the compiler in the same binary ──── *)
|
||||
|
||||
@ -5172,7 +5203,7 @@ let merged_setup () =
|
||||
{ session; child = None; agent; dir; stdout = rd;
|
||||
out = Buffer.create 4096; n = 0; gen = 0; owners = Hashtbl.create 32;
|
||||
host_ll; host_exe = exe; finished = false; agent_watch = None;
|
||||
park_noted = false }
|
||||
park_noted = false; died = None }
|
||||
in
|
||||
ignore_sigpipe ();
|
||||
(try Unix.unlink sock with Unix.Unix_error _ -> ());
|
||||
@ -5228,12 +5259,18 @@ let merged_serve () =
|
||||
this process. See [agent_check] for where the sentence is said now, and
|
||||
[eval] for what a delivery to such a program honestly reports. *)
|
||||
t.agent_watch <- Some (Unix.gettimeofday () +. 10.);
|
||||
(match accept_loop t ls with
|
||||
| () -> ()
|
||||
| exception e ->
|
||||
Printf.eprintf "flan dev: %s\n%!" (Printexc.to_string e));
|
||||
let clean =
|
||||
match accept_loop t ls with
|
||||
| () -> true
|
||||
| exception e ->
|
||||
Printf.eprintf "flan dev: %s\n%!" (Printexc.to_string e);
|
||||
false
|
||||
in
|
||||
(try Unix.close ls with Unix.Unix_error _ -> ());
|
||||
(try Unix.unlink sock with Unix.Unix_error _ -> ());
|
||||
(* The program is this process, so a program that crashed never gets here;
|
||||
the one end that does and is not clean is the loop raising. *)
|
||||
if clean then remove_session_dirs t;
|
||||
(* [close] from the editor ends the session, and so now does an editor that
|
||||
stopped being there; in one process either means the program too — which
|
||||
is what the daemon did by killing its child. [_exit] for the loader-lock
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user