From 612a98cfdec2bf97bb6a61573b4572684ca2a378 Mon Sep 17 00:00:00 2001 From: jutty Date: Wed, 9 Apr 2025 01:59:32 -0300 Subject: [PATCH] Implement most of the spec reusing scribe & nefthera code --- ocaml/TODO.md | 13 +++++++++ ocaml/bin/dune | 7 +++-- ocaml/bin/main.ml | 7 ++++- ocaml/dune-project | 29 ++++++++++--------- ocaml/dune-workspace | 4 +++ ocaml/lib/dune | 6 +++- ocaml/lib/parsers/argument.ml | 23 +++++++++++++++ ocaml/lib/qol.ml | 2 ++ ocaml/lib/schema/schema.ml | 38 +++++++++++++++++++++++++ ocaml/lib/system/file.ml | 14 ++++++++++ ocaml/lib/system/package.ml | 15 ++++++++++ ocaml/lib/system/process/fork.ml | 11 ++++++++ ocaml/lib/system/process/reader.ml | 45 ++++++++++++++++++++++++++++++ ocaml/test/dune | 4 ++- ocaml/test/test_tori.ml | 15 ++++++++++ ocaml/tori.opam | 17 +++++++---- 16 files changed, 225 insertions(+), 25 deletions(-) create mode 100644 ocaml/TODO.md create mode 100644 ocaml/dune-workspace create mode 100644 ocaml/lib/parsers/argument.ml create mode 100644 ocaml/lib/qol.ml create mode 100644 ocaml/lib/schema/schema.ml create mode 100644 ocaml/lib/system/file.ml create mode 100644 ocaml/lib/system/package.ml create mode 100644 ocaml/lib/system/process/fork.ml create mode 100644 ocaml/lib/system/process/reader.ml diff --git a/ocaml/TODO.md b/ocaml/TODO.md new file mode 100644 index 0000000..3c79acd --- /dev/null +++ b/ocaml/TODO.md @@ -0,0 +1,13 @@ +- [ ] Match test coverage with spec requirements +- [ ] Simplify Reader +- [ ] Create interface files + - [ ] Move comment on top of Parsers.Argument.say to the interface doc file +- [ ] Try out doc generation +- [ ] Simplify and analyze System.File +- [ ] Add log function and match spec + - [ ] Output begins with ' [log] ' + - [ ] Only prints if DEBUG is set +- [ ] Get su command from $XDG_CONFIG_HOME/tori/tori.conf and use it for pkg + - [ ] Check if working +- [ ] Command 'user': print the output of 'whoami' +- [ ] Unrecognized command: exit code 1 diff --git a/ocaml/bin/dune b/ocaml/bin/dune index d19048e..7546de9 100644 --- a/ocaml/bin/dune +++ b/ocaml/bin/dune @@ -1,4 +1,5 @@ (executable - (public_name tori) - (name main) - (libraries tori)) + (public_name tori) + (name main) + (libraries tori) +) diff --git a/ocaml/bin/main.ml b/ocaml/bin/main.ml index 4ff50b1..1e586af 100644 --- a/ocaml/bin/main.ml +++ b/ocaml/bin/main.ml @@ -1 +1,6 @@ -let () = print_endline "Hello, OCaml!" +let () = + match Array.to_list Sys.argv with + | _ :: tail -> + let future = (Tori.Parsers.Argument.interpret Tori.Schema.seed tail) in + if future.output.message <> "" then print_endline future.output.message + | [] -> assert false diff --git a/ocaml/dune-project b/ocaml/dune-project index 47fb9c4..a1e3d6d 100644 --- a/ocaml/dune-project +++ b/ocaml/dune-project @@ -3,25 +3,28 @@ (name tori) (version 0.8.0) -(generate_opam_files true) - (homepage https://tori.jutty.dev) (source (uri git+https://brew.bsd.cafe/tori/tori.git)) - (authors "Juno Takano ") - (maintainers "Juno Takano ") - (license GPL-3.0-only) - (documentation https://tori.jutty.dev/docs) (package - (name tori) - (synopsis "Track system configurations and replicate them") - (description "tori lets you define several characteristics of a unix system and track how they change along time, tracking how they changed and choosing whether or not to commit them. It aims for portability and declarative configuration practices, meaning you tell it what your system is, not how to reproduce it.") - (depends ocaml) - (tags - ("operating systems" "unix" "configuration management"))) + (name tori) + (synopsis "Track system configurations and replicate them") + (description + "\> tori lets you define several characteristics of a unix system and + "\> track changes happen along time, allowing you to choose whether or + "\> not to commit. It aims for portability and declarative configuration + "\> practices, meaning you tell it what your system is, not how to + "\> reproduce it. + ) + (tags ("operating systems" "unix" "configuration management")) + (depends + (ocaml (>= 5.3.0)) + (dune (>= 3.17.2)) + ) +) -; See the complete stanza docs at https://dune.readthedocs.io/en/stable/reference/dune-project/index.html +(generate_opam_files true) diff --git a/ocaml/dune-workspace b/ocaml/dune-workspace new file mode 100644 index 0000000..e2cca56 --- /dev/null +++ b/ocaml/dune-workspace @@ -0,0 +1,4 @@ +(lang dune 3.17) +(context default) + +(env (dev (flags (:standard -w +A-40-42)))) diff --git a/ocaml/lib/dune b/ocaml/lib/dune index 0945884..8c5b22d 100644 --- a/ocaml/lib/dune +++ b/ocaml/lib/dune @@ -1,2 +1,6 @@ (library - (name tori)) + (name tori) + (libraries unix) +) + +(include_subdirs qualified) diff --git a/ocaml/lib/parsers/argument.ml b/ocaml/lib/parsers/argument.ml new file mode 100644 index 0000000..10c1fb6 --- /dev/null +++ b/ocaml/lib/parsers/argument.ml @@ -0,0 +1,23 @@ +let interpret (past: Schema.schema) (input: string list): Schema.schema = + + let future = { past with output = { message = "" } } in + + (* say is useful when the only change to future is the output message *) + let say (message: string) = + { future with output = { message = message } } in + + (* + TODO: return a schema with orders, instead of calling side-effects + directly, making this more of a parser and less of a glorified switch + *) + match input with + | "pkg" :: tail -> System.Package.merge past tail + | "os" :: _ -> say (System.File.read "/etc/os-release") + | "host" :: _ -> say (System.Process.Reader.read [||] "hostname").output + | "echo" :: tail -> say (String.concat " " tail) + | ("version" | "-v" | "--version") :: _ -> + say (Schema.format_version future.meta.version) + | ("help" | "-h" | "--help") :: _ -> say future.meta.help.long + | head :: _ -> + say ("Unrecognized command: " ^ head ^ "\n" ^ future.meta.help.short) + | _ -> future diff --git a/ocaml/lib/qol.ml b/ocaml/lib/qol.ml new file mode 100644 index 0000000..0ebb240 --- /dev/null +++ b/ocaml/lib/qol.ml @@ -0,0 +1,2 @@ +let str_int = string_of_int +let print = print_endline diff --git a/ocaml/lib/schema/schema.ml b/ocaml/lib/schema/schema.ml new file mode 100644 index 0000000..9228987 --- /dev/null +++ b/ocaml/lib/schema/schema.ml @@ -0,0 +1,38 @@ +open Qol + +type version = { major: int; minor: int; patch: int } +type help = { short: string; long: string } +type meta = { version: version; help: help } + +type output = { message: string; } + +type os = Unknown | FreeBSD | Void | Alpine +type host = { os: os; name: string; } + +type schema = { meta: meta; output: output; host: host; } + +let seed: schema = { + meta = { + version = { + major = 0; + minor = 8; + patch = 0; + }; + help = { + short = "Use 'tori help' for usage instructions"; + long = ""; + }; + }; + output = { + message = "Use command 'help' for help"; + }; + host = { + os = Unknown; + name = "Unknown Host"; + }; +} + +let format_version (version: version): string = + "v" ^ str_int version.major ^ + "." ^ str_int version.minor ^ + "." ^ str_int version.patch diff --git a/ocaml/lib/system/file.ml b/ocaml/lib/system/file.ml new file mode 100644 index 0000000..efbf033 --- /dev/null +++ b/ocaml/lib/system/file.ml @@ -0,0 +1,14 @@ +let read_channel channel = + let buffer = Buffer.create 4096 in + let rec read () = + let line = input_line channel in + Buffer.add_string buffer line; + Buffer.add_char buffer '\n'; + read () + in + try read () with + End_of_file -> Buffer.contents buffer + +let read path = + let channel = open_in path in + read_channel channel diff --git a/ocaml/lib/system/package.ml b/ocaml/lib/system/package.ml new file mode 100644 index 0000000..37d0a14 --- /dev/null +++ b/ocaml/lib/system/package.ml @@ -0,0 +1,15 @@ +let merge (schema: Schema.schema) (packages: string list) = + + match packages with + | [] -> { schema with output = { message = "No packages provided" } } + | _ -> + + let in_targets = List.flatten [["doas"; "apk"; "-i"; "add"]; packages] and + out_targets = List.flatten [["doas"; "apk"; "-i"; "del"]; packages] in + + Process.Fork.run "doas" in_targets; + Process.Fork.run "doas" out_targets; + + { schema with output = { + message = "Done: " ^ (String.concat "\n" packages) + }} diff --git a/ocaml/lib/system/process/fork.ml b/ocaml/lib/system/process/fork.ml new file mode 100644 index 0000000..ca84c0f --- /dev/null +++ b/ocaml/lib/system/process/fork.ml @@ -0,0 +1,11 @@ +open Qol + +let run (command: string) (arguments: string list) = + match Unix.fork () with + | 0 -> Unix.execvp command (Array.of_list arguments) + | pid -> let (_, status) = Unix.waitpid [] pid in + match status with + | Unix.WEXITED 0 -> () + | Unix.WEXITED n -> print ("Process exited with code " ^ str_int n) + | Unix.WSIGNALED n -> print ("Process terminated by signal " ^ str_int n) + | Unix.WSTOPPED n -> print ("Process stopped by signal " ^ str_int n) diff --git a/ocaml/lib/system/process/reader.ml b/ocaml/lib/system/process/reader.ml new file mode 100644 index 0000000..1ee05f3 --- /dev/null +++ b/ocaml/lib/system/process/reader.ml @@ -0,0 +1,45 @@ +open Qol + +type output = { output: string; error: string; status: string; } + +let handle_exit_status (status: Unix.process_status): string = + match status with + | Unix.WEXITED n -> "Exit " ^ str_int n + | Unix.WSIGNALED n -> "Kill " ^ str_int n + | Unix.WSTOPPED n -> "Stopped " ^ str_int n + +let read (env: string array) (command: string): output = + + let stdout, stdin, stderr = Unix.open_process_full command env in + let in_buffer = Buffer.create 4096 in + let err_buffer = Buffer.create 4096 in + + let rec read_in () = + let in_line = input_line stdout in + Buffer.add_string in_buffer in_line; + Buffer.add_char in_buffer '\n'; + read_in () + in + try read_in () with End_of_file -> (); + + let rec read_err () = + let err_line = input_line stderr in + Buffer.add_string err_buffer err_line; + Buffer.add_char err_buffer '\n'; + read_err () + in + try read_err () with + End_of_file -> let exit_status = + handle_exit_status (Unix.close_process_full (stdout, stdin, stderr)) + in + { + output = String.trim (Buffer.contents in_buffer); + error = Buffer.contents err_buffer; + status = exit_status; + } + +let format (output: output): string = + match output with + | { output = o; error = _; status = "Exit 0" } -> o + | { output = ""; error = e; status = s } -> "[" ^ s ^ "]" ^ " " ^ e + | { output = o; error = _; status = s } -> "[" ^ s ^ "]" ^ " " ^ o diff --git a/ocaml/test/dune b/ocaml/test/dune index 807a851..002c77c 100644 --- a/ocaml/test/dune +++ b/ocaml/test/dune @@ -1,2 +1,4 @@ (test - (name test_tori)) + (name test_tori) + (libraries tori) +) diff --git a/ocaml/test/test_tori.ml b/ocaml/test/test_tori.ml index e69de29..4a03b32 100644 --- a/ocaml/test/test_tori.ml +++ b/ocaml/test/test_tori.ml @@ -0,0 +1,15 @@ +module Reader = Tori.System.Process.Reader +module File = Tori.System.File + +let smoke () = + + (* Executing echo should return the same string on output *) + let result = Reader.read [||] "echo 0x70121" in + assert (Reader.format result = "0x70121"); + + (* Reading a file, relying on Dune's directory structure *) + let file_contents = File.read "../tori.opam" in + let contents_list = String.split_on_char '\n' file_contents in + assert (List.mem "depends: [" contents_list) + +let () = smoke () diff --git a/ocaml/tori.opam b/ocaml/tori.opam index dc0dfaa..baff66b 100644 --- a/ocaml/tori.opam +++ b/ocaml/tori.opam @@ -1,18 +1,23 @@ # This file is generated by dune, edit dune-project instead opam-version: "2.0" version: "0.8.0" -synopsis: "A tool to track system configurations and replicate them." -description: - "tori lets you define several characteristics of a unix system and track how they change along time, tracking how they changed and choosing whether or not to commit them. It aims for portability and declarative configuration practices, meaning you tell it what your system is, not how to reproduce it." +synopsis: "Track system configurations and replicate them" +description: """ +tori lets you define several characteristics of a unix system and +track changes happen along time, allowing you to choose whether or +not to commit. It aims for portability and declarative configuration +practices, meaning you tell it what your system is, not how to +reproduce it. +""" maintainer: ["Juno Takano "] authors: ["Juno Takano "] -license: "GPL-3.5-only" +license: "GPL-3.0-only" tags: ["operating systems" "unix" "configuration management"] homepage: "https://tori.jutty.dev" doc: "https://tori.jutty.dev/docs" depends: [ - "dune" {>= "3.17"} - "ocaml" + "ocaml" {>= "5.3.0"} + "dune" {>= "3.17" & >= "3.17.2"} "odoc" {with-doc} ] build: [