diff --git a/bin/.ocamlformat b/bin/.ocamlformat new file mode 100644 index 0000000..e69de29 diff --git a/bin/dune b/bin/dune new file mode 100644 index 0000000..b1f555a --- /dev/null +++ b/bin/dune @@ -0,0 +1,4 @@ +(executable + (public_name meat) + (name main) + (libraries meat)) diff --git a/bin/main.ml b/bin/main.ml new file mode 100644 index 0000000..11c0cca --- /dev/null +++ b/bin/main.ml @@ -0,0 +1,21 @@ +open Meat + +let () = + match Sys.getenv_opt "MEATS" with + | Some _ -> + if Array.length Sys.argv >= 2 then + match String.lowercase_ascii (Array.get Sys.argv 1) with + | "yum" -> yum () + | "cook" -> cook () + | "poke" -> poke () + | "gut" -> gut () + | "trade" -> trade () + | "look" -> look () + | "fresh" -> fresh () + | "hunt" -> hunt () + | "ritual" -> ritual () + | _ -> help () + else help () + | None -> + meat_print "NO PATH TO RUNESTONE FOUND!"; + help () diff --git a/dune-project b/dune-project new file mode 100644 index 0000000..256991c --- /dev/null +++ b/dune-project @@ -0,0 +1,26 @@ +(lang dune 3.16) + +(name meat) + +(generate_opam_files true) + +(source + (github username/reponame)) + +(authors "Author Name ") + +(maintainers "Maintainer Name ") + +(license LICENSE) + +(documentation https://url/to/documentation) + +(package + (name meat) + (synopsis "A short synopsis") + (description "A longer description") + (depends ocaml dune) + (tags + ("add topics" "to describe" your project))) + +; See the complete stanza docs at https://dune.readthedocs.io/en/stable/reference/dune-project/index.html diff --git a/flake.lock b/flake.lock index 863dc62..843a72b 100644 --- a/flake.lock +++ b/flake.lock @@ -5,11 +5,11 @@ "nixpkgs": "nixpkgs_2" }, "locked": { - "lastModified": 1779030999, - "narHash": "sha256-PLR0pNxIN3JPs/rSVnXTIPXzkPLaGAdt/wjPq7+k1PE=", + "lastModified": 1773478949, + "narHash": "sha256-8rMpSs2OWGaDlDFO5FS6Pf9WutHBXxM2omPr6hfKydI=", "owner": "feel-co", "repo": "ndg", - "rev": "b363612b524436520c85100192ec2bdab9a675c0", + "rev": "c3bc1541668e6f6632a7005c7e4963c0a5dedc7b", "type": "github" }, "original": { @@ -35,11 +35,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1778869304, - "narHash": "sha256-30sZNZoA1cqF5JNO9fVX+wgiQYjB7HJqqJ4ztCDeBZE=", + "lastModified": 1774078191, + "narHash": "sha256-nyxxxW1/2ouu9dU0I02ul5pHrmUrE1JVFhfFlmYe3Lw=", "owner": "nixos", "repo": "nixpkgs", - "rev": "d233902339c02a9c334e7e593de68855ad26c4cb", + "rev": "09061f748ee21f68a089cd5d91ec1859cd93d0be", "type": "github" }, "original": { @@ -51,11 +51,11 @@ }, "nixpkgs_2": { "locked": { - "lastModified": 1775036866, - "narHash": "sha256-ByAX1LkhCwZ94+KnFAmnJSMAvui7kgCxjHgUHsWAbfI=", - "rev": "6201e203d09599479a3b3450ed24fa81537ebc4e", + "lastModified": 1773282481, + "narHash": "sha256-oFe06TmOy8UUT1f7xMHqDpSYq2Fy1mkIsXZUvdnyfeY=", + "rev": "fe416aaedd397cacb33a610b33d60ff2b431b127", "type": "tarball", - "url": "https://releases.nixos.org/nixos/unstable/nixos-26.05pre972949.6201e203d095/nixexprs.tar.xz?lastModified=1775036866&rev=6201e203d09599479a3b3450ed24fa81537ebc4e" + "url": "https://releases.nixos.org/nixos/unstable/nixos-26.05pre962285.fe416aaedd39/nixexprs.tar.xz?lastModified=1773282481&rev=fe416aaedd397cacb33a610b33d60ff2b431b127" }, "original": { "type": "tarball", @@ -64,11 +64,11 @@ }, "nixpkgs_3": { "locked": { - "lastModified": 1778869304, - "narHash": "sha256-30sZNZoA1cqF5JNO9fVX+wgiQYjB7HJqqJ4ztCDeBZE=", + "lastModified": 1774078191, + "narHash": "sha256-nyxxxW1/2ouu9dU0I02ul5pHrmUrE1JVFhfFlmYe3Lw=", "owner": "nixos", "repo": "nixpkgs", - "rev": "d233902339c02a9c334e7e593de68855ad26c4cb", + "rev": "09061f748ee21f68a089cd5d91ec1859cd93d0be", "type": "github" }, "original": { diff --git a/flake.nix b/flake.nix index 263e27e..33d7c3a 100644 --- a/flake.nix +++ b/flake.nix @@ -10,26 +10,67 @@ with inputs; let version = builtins.toString self.lastModified; + collectPkgs = builtins.attrValues; forEachSystem = function: nixpkgs.lib.genAttrs (import nix-systems) ( system: function nixpkgs.legacyPackages.${system} system ); + deps = forEachSystem ( + pkgs: _: { + build = collectPkgs { + inherit (pkgs.ocamlPackages) + dune_3 + ocaml + yojson + ssl + ; + inherit (pkgs.openssl) dev; + }; + dev = collectPkgs { + inherit (pkgs.ocamlPackages) + utop + ocaml-lsp + ocamlformat + ocamlformat-rpc-lib + ; + }; + } + ); in { devShells = forEachSystem ( - pkgs: _: { + pkgs: sys: { default = pkgs.mkShell { - packages = [ - pkgs.nushell - ]; + packages = pkgs.lib.mapAttrsToList (_: v: v) deps.${sys}; + shellHook = + let + justFile = '' + default: + @just --list + + @build: + nix build .#debug --offline + + @release: + nix build --offline + + @test: + printf "\\n\\n\\t************ running nix+dune tests ************\\n\\n\\n" + nix flake check --offline + ''; + in + '' + printf '${justFile}' > justfile + ''; }; } ); packages = forEachSystem ( - pkgs: _: { + pkgs: sys: { default = pkgs.callPackage ./nix/default.nix { + buildInputs = deps.${sys}.build; inherit version; }; docs = pkgs.callPackage unf.lib.pak-chooie { @@ -47,18 +88,11 @@ { pkgs, lib, - config, ... }: - let - cfg = config.programs.meat; - in { imports = [ ./nix/module.nix ]; - programs.meat.package = self.packages.${pkgs.stdenv.hostPlatform.system}.default.overrideAttrs { - differ = cfg.differ; - monitor = cfg.monitor; - }; + programs.meat.package = self.packages.${pkgs.stdenv.hostPlatform.system}.default; }; }; diff --git a/lib/.ocamlformat b/lib/.ocamlformat new file mode 100644 index 0000000..e69de29 diff --git a/lib/commands/cook.ml b/lib/commands/cook.ml new file mode 100644 index 0000000..4c79b1c --- /dev/null +++ b/lib/commands/cook.ml @@ -0,0 +1,9 @@ +let run () = + print_string Common.header; + Common.meat_print "PREPARING DELICIOUS MEATS.."; + let build_target = + Unix.getenv "MEATS" ^ "#nixosConfigurations." ^ Unix.gethostname () + ^ ".config.system.build.toplevel" + in + Common.do_cmd @@ "nix build --no-link" ^ build_target |> ignore; + print_string Common.footer diff --git a/lib/commands/fresh.ml b/lib/commands/fresh.ml new file mode 100644 index 0000000..d4c4a52 --- /dev/null +++ b/lib/commands/fresh.ml @@ -0,0 +1,17 @@ +let run () = + print_string Common.header; + Common.meat_print "HUNTING FRESH MEATS.."; + let argv_len = Array.length Sys.argv in + let root = Sys.getenv "MEATS" in + if argv_len >= 3 then + let open Array in + let flakes = sub Sys.argv 2 (argv_len - 2) in + flakes + |> iter (fun f -> + if Common.all_low f = "meat" then Common.meat_print "PROCESSING REAL MEAT.." + else Common.meat_print ("PROCESSING FRESH MEAT " ^ Common.all_caps f ^ ".."); + Common.do_cmd ~args:false @@ "nix flake update " ^ f ^ " --flake " ^ root + |> ignore) + else Common.do_cmd @@ "nix flake update --flake " ^ root |> ignore; + print_string Common.footer; + print_newline () diff --git a/lib/commands/gut.ml b/lib/commands/gut.ml new file mode 100644 index 0000000..8a5594c --- /dev/null +++ b/lib/commands/gut.ml @@ -0,0 +1,5 @@ +let run () = + print_string Common.header; + Common.meat_print "CLEANING MEAT STORES.."; + Common.do_cmd "nh clean all" |> ignore; + print_string Common.footer diff --git a/lib/commands/help.ml b/lib/commands/help.ml new file mode 100644 index 0000000..f29a0e7 --- /dev/null +++ b/lib/commands/help.ml @@ -0,0 +1 @@ +let run () = print_string (Common.header ^ Common.help_text ^ Common.footer ^ "\n") diff --git a/lib/commands/hunt.ml b/lib/commands/hunt.ml new file mode 100644 index 0000000..655feee --- /dev/null +++ b/lib/commands/hunt.ml @@ -0,0 +1,61 @@ +let es_host = "nixos-search-7-1733963800.us-east-1.bonsaisearch.net" + +let es_query term = + Printf.sprintf + {|{"from":0,"size":20,"sort":[{"_score":"desc","package_attr_name":"desc","package_pversion":"desc"}],"collapse":{"field":"package_attr_name"},"query":{"bool":{"must":[{"term":{"type":"package"}},{"multi_match":{"type":"cross_fields","query":"%s","fields":["package_attr_name^9","package_pname^6","package_description^1.3","package_longDescription^1"]}}]}}}|} + term + +let truncate n s = if String.length s > n then String.sub s 0 n ^ ".." else s + +let run () = + print_string Common.header; + let argv_len = Array.length Sys.argv in + if argv_len < 3 then ( + Common.meat_print "WHAT MEAT ARE YOU HUNTING?"; + print_string Common.footer) + else + let query = + Array.sub Sys.argv 2 (argv_len - 2) + |> Array.to_list |> String.concat " " + in + Common.meat_print ("HUNTING FOR " ^ Common.all_caps query ^ ".."); + let body = es_query query in + let path = "/nixos-*-unstable-*/_search" in + (try + let resp = Http.https_post ~host:es_host ~path ~body in + let json = Json.parse_json resp in + let hits = + match Option.bind (Json.jfield "hits" json) (fun h -> Json.jfield "hits" h) with + | Some (`List items) -> items + | _ -> [] + in + if hits = [] then + Common.meat_print "NO MEATS FOUND!" + else + List.iter + (fun hit -> + let src = + match Json.jfield "_source" hit with Some s -> s | None -> `Null + in + let name = + match Option.bind (Json.jfield "package_attr_name" src) Json.jstring with + | Some s -> s + | None -> "?" + in + let version = + match Option.bind (Json.jfield "package_pversion" src) Json.jstring with + | Some s -> s + | None -> "" + in + let desc = + match Option.bind (Json.jfield "package_description" src) Json.jstring with + | Some s -> truncate 60 s + | None -> "" + in + let ver_str = if version <> "" then " (" ^ version ^ ")" else "" in + Printf.printf " \tnixpkgs#%s%s\n" name ver_str; + if desc <> "" then Printf.printf " \t %s\n" desc) + hits + with e -> + Common.meat_print ("HUNT FAILED: " ^ Printexc.to_string e)); + print_string Common.footer diff --git a/lib/commands/look.ml b/lib/commands/look.ml new file mode 100644 index 0000000..c1df668 --- /dev/null +++ b/lib/commands/look.ml @@ -0,0 +1,114 @@ +let run () = + print_string Common.header; + Common.meat_print "LOOKING FOR FRESHER MEATS.."; + let lockfile = Http.read_file (Unix.getenv "MEATS" ^ "/flake.lock") in + let json = Json.parse_json lockfile in + let nodes = + match Json.jfield "nodes" json with Some n -> n | None -> failwith "no nodes" + in + let root_node = + match Json.jfield "root" nodes with Some n -> n | None -> failwith "no root" + in + let root_inputs = + match Json.jfield "inputs" root_node with + | Some n -> n + | None -> failwith "no root inputs" + in + let input_pairs = + match Json.jassoc root_inputs with + | Some p -> p + | None -> failwith "root inputs not object" + in + let any_stale = ref false in + List.iter + (fun (name, node_ref) -> + let node_name = + match Json.jstring node_ref with Some s -> s | None -> name + in + let node = + match Json.jfield node_name nodes with + | Some n -> n + | None -> failwith ("no node: " ^ node_name) + in + let locked = + match Json.jfield "locked" node with + | Some n -> n + | None -> failwith "no locked" + in + let original = + match Json.jfield "original" node with + | Some n -> n + | None -> failwith "no original" + in + let locked_rev = + match Option.bind (Json.jfield "rev" locked) Json.jstring with + | Some s -> s + | None -> failwith "no rev" + in + let locked_type = + match Option.bind (Json.jfield "type" locked) Json.jstring with + | Some s -> s + | None -> failwith "no type" + in + let original_ref = Option.bind (Json.jfield "ref" original) Json.jstring in + let host, path = + match locked_type with + | "github" -> + let owner = + match Option.bind (Json.jfield "owner" locked) Json.jstring with + | Some s -> s + | None -> failwith "no owner" + in + let repo = + match Option.bind (Json.jfield "repo" locked) Json.jstring with + | Some s -> s + | None -> failwith "no repo" + in + ( "github.com", + "/" ^ owner ^ "/" ^ repo + ^ ".git/info/refs?service=git-upload-pack" ) + | "git" -> + let url = + match Option.bind (Json.jfield "url" original) Json.jstring with + | Some s -> s + | None -> failwith "no url" + in + let prefix = "https://" in + let plen = String.length prefix in + if String.length url > plen && String.sub url 0 plen = prefix then + let after = + String.sub url plen (String.length url - plen) + in + let slash = + match String.index_opt after '/' with + | Some i -> i + | None -> failwith "no path in url" + in + let h = String.sub after 0 slash in + let p = + String.sub after slash (String.length after - slash) + in + (h, p ^ "/info/refs?service=git-upload-pack") + else failwith ("unsupported url: " ^ url) + | t -> failwith ("unsupported type: " ^ t) + in + try + let body = Http.https_get ~host ~path in + let refs = Http.parse_pktline body in + let target_ref = + match original_ref with + | Some r -> "refs/heads/" ^ r + | None -> "HEAD" + in + let latest_rev = + match List.assoc_opt target_ref refs with + | Some rev -> rev + | None -> failwith ("ref not found: " ^ target_ref) + in + if latest_rev <> locked_rev then ( + any_stale := true; + Common.meat_print (Common.all_caps name ^ " HAS FRESHER MEAT!")) + with _ -> Common.meat_print ("COULD NOT REACH " ^ Common.all_caps name ^ "..")) + input_pairs; + if not !any_stale then Common.meat_print "ALL MEATS ARE FRESH!"; + print_string Common.footer diff --git a/lib/commands/poke.ml b/lib/commands/poke.ml new file mode 100644 index 0000000..800e39d --- /dev/null +++ b/lib/commands/poke.ml @@ -0,0 +1,9 @@ +let run () = + print_string Common.header; + Common.meat_print "PREPARING SUSPICIOUS MEATS.."; + let build_target = + Unix.getenv "MEATS" ^ "#nixosConfigurations." ^ Unix.gethostname () + ^ ".config.system.build.toplevel" + in + Common.do_cmd @@ "nix build --no-link " ^ build_target ^ " --show-trace" |> ignore; + print_string Common.footer diff --git a/lib/commands/ritual.ml b/lib/commands/ritual.ml new file mode 100644 index 0000000..739995f --- /dev/null +++ b/lib/commands/ritual.ml @@ -0,0 +1,39 @@ +let run () = + print_string Common.header; + Common.meat_print "PREPARING RITUAL GROUND.."; + let ( >>= ) = Result.bind in + let ( >|= ) = Fun.flip Result.map in + let tmpdir = Filename.temp_dir "meat-chew" "" in + let meats = Unix.getenv "MEATS" in + let hostname = Unix.gethostname () in + let nix_conf_target = + meats ^ "/entry.nix -A nixosConfigurations." ^ hostname + ^ {|'.config.environment.etc."nix/nix.conf"'|} + in + let build_target = + meats ^ "/entry.nix -A nixosConfigurations." ^ hostname + ^ ".config.system.build.toplevel" + in + ( Common.do_cmd ~args:false + @@ "nix-build --log-format internal-json -v --out-link " ^ tmpdir + ^ "/nix.conf " ^ nix_conf_target ^ " |& nom --json" + >>= fun () -> + Common.meat_print "CONSUMING MEATS.."; + Common.do_cmd ~args:false + @@ "NIX_USER_CONF_FILES=" ^ tmpdir + ^ "/nix.conf nix-build --log-format internal-json -v --out-link " ^ tmpdir + ^ "/build " ^ build_target ^ " |& nom --json" + >>= fun () -> + Common.do_cmd ~args:false @@ "dix /nix/var/nix/profiles/system " ^ tmpdir + ^ "/build" + >>= fun () -> + Common.do_cmd ~args:false + @@ "sudo sh -c 'nix-env --set -p /nix/var/nix/profiles/system " ^ tmpdir + ^ "/build && " ^ tmpdir ^ "/build/bin/switch-to-configuration switch'" + >|= fun () -> + Unix.unlink @@ tmpdir ^ "/nix-conf"; + Unix.unlink @@ tmpdir ^ "/build" ) + |> ( function + | Error _ -> print_string "FAILED TO CONSUME MEATS." + | _ -> () ); + print_string Common.footer diff --git a/lib/commands/trade.ml b/lib/commands/trade.ml new file mode 100644 index 0000000..80e6dfa --- /dev/null +++ b/lib/commands/trade.ml @@ -0,0 +1,8 @@ +let run () = + print_string Common.header; + Common.meat_print "TRADING FOREIGN MEATS.."; + Common.do_remote () |> function + | Error _ -> print_string "FAILED TO TRADE MEATS." + | _ -> + (); + print_string Common.footer diff --git a/lib/commands/yum.ml b/lib/commands/yum.ml new file mode 100644 index 0000000..08514cc --- /dev/null +++ b/lib/commands/yum.ml @@ -0,0 +1,7 @@ +let run () = + print_string Common.header; + Common.meat_print "CONSUMING DELICIOUS MEATS.."; + ( Common.do_build () |> function + | Error _ -> print_string "FAILED TO CONSUME MEATS." + | _ -> () ); + print_string Common.footer diff --git a/lib/common.ml b/lib/common.ml new file mode 100644 index 0000000..ca7ead8 --- /dev/null +++ b/lib/common.ml @@ -0,0 +1,61 @@ +let header = "\n ----- MEAT ----------------------------------------\n" +let footer = "\n ---------------------------------------------------\n" + +let help_text = + {| + YUM - CONSUME DELICIOUS MEATS + COOK - ONLY PREPARE MEATS + POKE - TASTE SUSPICIOUS MEATS + GUT - CLEAN MEAT STORES + FRESH - HUNT FRESH MEATS + LOOK - LOOK FOR FRESHER MEATS + HUNT - HUNT FOR MEATS IN NIXPKGS + RITUAL - PERFORM RITUAL THEN CONSUME + TRADE - SEND MEATS FAR AWAY + ..-A - ..ALL MEATS|} +open Sys + +let pass_args () = + let len = Array.length argv and sconcat acc el = acc ^ " " ^ el in + match len with + | 3 -> argv.(2) + | n when n > 3 -> + print_int (n - 1); + Array.fold_left sconcat " " (Array.sub argv 2 (n - 2)) + | _ -> "" + +let do_cmd ?(args = true) cmd = + match command (if args then cmd ^ " " ^ pass_args () else cmd) with + | 0 -> Ok () + | e -> Error e + +let meat_print text = print_endline ("\n \t" ^ text ^ "\n") + +let do_build () = + let ( >>= ) = Result.bind in + let ( >|= ) = Fun.flip Result.map in + let tmpdir = Filename.temp_dir "meat-build" "" in + let build_target = + Unix.getenv "MEATS" ^ "/entry.nix -A nixosConfigurations." + ^ Unix.gethostname () ^ ".config.system.build.toplevel" + in + do_cmd @@ "nix-build --log-format internal-json -v --out-link " ^ tmpdir + ^ "/build " ^ build_target ^ " |& nom --json" + >>= fun () -> + do_cmd @@ "dix /nix/var/nix/profiles/system " ^ tmpdir ^ "/build" + >>= fun () -> + do_cmd @@ "sudo sh -c 'nix-env --set -p /nix/var/nix/profiles/system " ^ tmpdir + ^ "/build && " ^ tmpdir ^ "/build/bin/switch-to-configuration switch'" + >|= fun () -> Unix.unlink @@ tmpdir ^ "/build" + +let do_remote () = + meat_print "tbd"; + Ok () + +let all_flag () = + if Array.length argv >= 3 then + match Array.get argv 2 with "-a" | "--all" -> true | _ -> false + else false + +let all_caps s = s |> String.map (fun c -> Char.uppercase_ascii c) +let all_low s = s |> String.map (fun c -> Char.lowercase_ascii c) diff --git a/lib/dune b/lib/dune new file mode 100644 index 0000000..016c372 --- /dev/null +++ b/lib/dune @@ -0,0 +1,4 @@ +(include_subdirs qualified) +(library + (name meat) + (libraries unix ssl yojson)) diff --git a/lib/http.ml b/lib/http.ml new file mode 100644 index 0000000..f681505 --- /dev/null +++ b/lib/http.ml @@ -0,0 +1,232 @@ +let read_file path = + let ic = open_in path in + let n = in_channel_length ic in + let s = really_input_string ic n in + close_in ic; + s + +let contains_sub haystack needle = + let hl = String.length haystack and nl = String.length needle in + if nl > hl then false + else + let rec check i = + if i + nl > hl then false + else if String.sub haystack i nl = needle then true + else check (i + 1) + in + check 0 + +let decode_chunked body = + let len = String.length body in + let buf = Buffer.create (len / 2) in + let i = ref 0 in + (try + while !i < len do + let line_end = + let rec find j = + if j >= len then raise Exit else if body.[j] = '\r' then j + else find (j + 1) + in + find !i + in + let size_str = String.sub body !i (line_end - !i) in + let size_str = + match String.index_opt size_str ';' with + | Some idx -> String.sub size_str 0 idx + | None -> size_str + in + let size = int_of_string ("0x" ^ String.trim size_str) in + if size = 0 then raise Exit; + i := line_end + 2; + if !i + size <= len then + Buffer.add_string buf (String.sub body !i size); + i := !i + size + 2 + done + with Exit -> ()); + Buffer.contents buf + +let b64_encode src = + let tbl = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/" in + let len = String.length src in + let buf = Buffer.create (len * 4 / 3 + 4) in + let i = ref 0 in + while !i + 2 < len do + let a = Char.code src.[!i] and b = Char.code src.[!i+1] and c = Char.code src.[!i+2] in + Buffer.add_char buf tbl.[(a lsr 2) land 0x3f]; + Buffer.add_char buf tbl.[((a lsl 4) lor (b lsr 4)) land 0x3f]; + Buffer.add_char buf tbl.[((b lsl 2) lor (c lsr 6)) land 0x3f]; + Buffer.add_char buf tbl.[c land 0x3f]; + i := !i + 3 + done; + (match len - !i with + | 2 -> + let a = Char.code src.[!i] and b = Char.code src.[!i+1] in + Buffer.add_char buf tbl.[(a lsr 2) land 0x3f]; + Buffer.add_char buf tbl.[((a lsl 4) lor (b lsr 4)) land 0x3f]; + Buffer.add_char buf tbl.[(b lsl 2) land 0x3f]; + Buffer.add_char buf '=' + | 1 -> + let a = Char.code src.[!i] in + Buffer.add_char buf tbl.[(a lsr 2) land 0x3f]; + Buffer.add_char buf tbl.[(a lsl 4) land 0x3f]; + Buffer.add_string buf "==" + | _ -> ()); + Buffer.contents buf + +let ssl_inited = ref false + +let https_request ~meth ~host ~path ?(headers=[]) ?(body="") () = + if not !ssl_inited then ( + Ssl.init (); + ssl_inited := true); + let he = Unix.gethostbyname host in + let addr = he.h_addr_list.(0) in + let sockaddr = Unix.ADDR_INET (addr, 443) in + let ctx = Ssl.create_context Ssl.TLSv1_2 Ssl.Client_context in + let sock = Unix.socket Unix.PF_INET Unix.SOCK_STREAM 0 in + Unix.connect sock sockaddr; + let ssl = Ssl.embed_socket sock ctx in + Ssl.set_client_SNI_hostname ssl host; + Ssl.connect ssl; + let extra_headers = + List.fold_left (fun acc (k, v) -> acc ^ k ^ ": " ^ v ^ "\r\n") "" headers + in + let content_len = if body <> "" then + Printf.sprintf "Content-Length: %d\r\n" (String.length body) + else "" in + let req = + Printf.sprintf "%s %s HTTP/1.1\r\nHost: %s\r\nUser-Agent: meat/1.0\r\nConnection: close\r\n%s%s\r\n%s" + meth path host extra_headers content_len body + in + ignore (Ssl.write ssl (Bytes.of_string req) 0 (String.length req)); + let buf = Buffer.create 8192 in + let chunk = Bytes.create 4096 in + (try + while true do + let n = Ssl.read ssl chunk 0 4096 in + if n = 0 then raise Exit; + Buffer.add_subbytes buf chunk 0 n + done + with _ -> ()); + (try Ssl.shutdown_connection ssl with _ -> ()); + Unix.close sock; + let response = Buffer.contents buf in + let rlen = String.length response in + let header_end = + let rec find pos = + if pos + 3 >= rlen then rlen + else if + response.[pos] = '\r' + && response.[pos + 1] = '\n' + && response.[pos + 2] = '\r' + && response.[pos + 3] = '\n' + then pos + else find (pos + 1) + in + find 0 + in + let resp_headers = String.sub response 0 header_end in + let body_start = min (header_end + 4) rlen in + let resp_body = String.sub response body_start (rlen - body_start) in + if + contains_sub + (String.lowercase_ascii resp_headers) + "transfer-encoding: chunked" + then decode_chunked resp_body + else resp_body + +let https_get ~host ~path = + https_request ~meth:"GET" ~host ~path + ~headers:[("User-Agent", "git/2.0")] () + +let https_post ~host ~path ~body = + https_request ~meth:"POST" ~host ~path + ~headers:[ + ("Content-Type", "application/json"); + ("Accept", "application/json"); + ("Authorization", "Basic " ^ b64_encode "aWVSALXpZv:X8gPHnzL52wFEekuxsfQ9cSh"); + ] ~body () + +let parse_pktline body = + let len = String.length body in + let i = ref 0 in + let refs = ref [] in + let hex_val c = + match c with + | '0' .. '9' -> Char.code c - Char.code '0' + | 'a' .. 'f' -> Char.code c - Char.code 'a' + 10 + | 'A' .. 'F' -> Char.code c - Char.code 'A' + 10 + | _ -> 0 + in + let read_pkt_len () = + if !i + 4 > len then 0 + else + let a = hex_val body.[!i] + and b = hex_val body.[!i + 1] + and c = hex_val body.[!i + 2] + and d = hex_val body.[!i + 3] in + i := !i + 4; + (a * 4096) + (b * 256) + (c * 16) + d + in + let plen = read_pkt_len () in + if plen > 4 then i := !i + (plen - 4); + ignore (read_pkt_len ()); + let continue = ref true in + while !continue do + let plen = read_pkt_len () in + if plen = 0 then continue := false + else + let payload_len = plen - 4 in + if !i + payload_len <= len && payload_len >= 41 then ( + let payload = String.sub body !i payload_len in + i := !i + payload_len; + let sha = String.sub payload 0 40 in + let rest = String.sub payload 41 (String.length payload - 41) in + let refname = + let s = + match String.index_opt rest '\000' with + | Some idx -> String.sub rest 0 idx + | None -> rest + in + match String.index_opt s '\n' with + | Some idx -> String.sub s 0 idx + | None -> s + in + refs := (refname, sha) :: !refs) + else i := !i + max 0 payload_len + done; + List.rev !refs + +let derelativise base = List.map (fun a -> base ^ "/" ^ a) + +let filter_dirs fullpath dirs = + dirs |> derelativise fullpath + |> List.filter (fun d -> (Unix.stat d).st_kind = Unix.S_DIR) + +let readdir d = try Sys.readdir d with Sys_error _ -> [||] + +let walk entry = + let open List in + let rec loop dir : string list = + let contents = readdir dir |> Array.to_list in + let is_flake = mem "flake.nix" contents in + if dir = entry then + let subdirs = contents |> filter_dirs dir in + flatten (map loop subdirs) + else if is_flake then + let subdirs = contents |> filter_dirs dir in + let children = flatten (map loop subdirs) in + [ dir ] @ children + else [] + in + loop entry + +let countdepth s = + s |> String.fold_left (fun acc el -> acc + if el = '/' then 1 else 0) 0 + +let compdepth a b = + let ad = countdepth a and bd = countdepth b in + let dif = ad - bd in + match dif with 0 -> 0 | _ -> dif / abs dif + +let fmt_dir d = String.split_on_char '/' d |> List.rev |> List.hd |> Common.all_caps diff --git a/lib/json.ml b/lib/json.ml new file mode 100644 index 0000000..5dd5297 --- /dev/null +++ b/lib/json.ml @@ -0,0 +1,8 @@ +let parse_json src = Yojson.Basic.from_string src + +let jfield key = function + | `Assoc pairs -> List.assoc_opt key pairs + | _ -> None + +let jstring = function `String s -> Some s | _ -> None +let jassoc = function `Assoc pairs -> Some pairs | _ -> None diff --git a/lib/meat.ml b/lib/meat.ml new file mode 100644 index 0000000..29a35d1 --- /dev/null +++ b/lib/meat.ml @@ -0,0 +1,11 @@ +let yum = Commands.Yum.run +let cook = Commands.Cook.run +let poke = Commands.Poke.run +let gut = Commands.Gut.run +let trade = Commands.Trade.run +let look = Commands.Look.run +let fresh = Commands.Fresh.run +let hunt = Commands.Hunt.run +let ritual = Commands.Ritual.run +let help = Commands.Help.run +let meat_print = Common.meat_print diff --git a/meat.nu b/meat.nu deleted file mode 100644 index c0ecbfb..0000000 --- a/meat.nu +++ /dev/null @@ -1,285 +0,0 @@ -#!/usr/bin/env nu - -const HEADER = " - ----- MEAT ---------------------------------------- -" - -const FOOTER = " - --------------------------------------------------- -" - -const HELP_TEXT = " - YUM - CONSUME DELICIOUS MEATS - COOK - ONLY PREPARE MEATS - POKE - TASTE SUSPICIOUS MEATS - GUT - CLEAN MEAT STORES - FRESH - HUNT FRESH MEATS - LOOK - LOOK FOR FRESHER MEATS - HUNT - HUNT FOR MEATS IN NIXPKGS - RITUAL - PERFORM RITUAL THEN CONSUME - TRADE - SEND MEATS FAR AWAY - ..-A - ..ALL MEATS" - -def meat-print [text: string] { - print $"\n \t($text)\n" -} - -def with-frame [body: closure] { - print -n $HEADER - do $body - print -n $FOOTER -} - -def hn []: nothing -> string { - ^hostname | str trim -} - -def flake-target []: nothing -> string { - $"($env.MEATS)#nixosConfigurations.(hn).config.system.build.toplevel" -} - -def system-attr []: nothing -> string { - $"nixosConfigurations.(hn).config.system.build.toplevel" -} - -def nix-conf-attr []: nothing -> string { - $"nixosConfigurations.(hn).config.environment.etc.\"nix/nix.conf\"" -} - -def nix-build-nom [out: string, source: string, attr: string, extras: list = []] { - ^nix-build --log-format internal-json -v --out-link $out $source -A $attr ...$extras out+err>| ^$env.MONITOR --json -} - -def activate [build_path: string] { - ^sudo $nu.current-exe -c $"nix-env --set -p /nix/var/nix/profiles/system ($build_path); ($build_path)/bin/switch-to-configuration switch" -} - -def do-build [extras: list = []] { - let tmpdir = ^mktemp -d -t "meat-build.XXXXXX" | str trim - let build = $"($tmpdir)/build" - nix-build-nom $build $"($env.MEATS)/entry.nix" (system-attr) $extras - ^$env.DIFFER /nix/var/nix/profiles/system $build - activate $build - try { rm $build } -} - -def cmd-help [] { - print -n $"($HEADER)($HELP_TEXT)($FOOTER)\n" -} - -def cmd-yum [...args: string] { - with-frame { - meat-print "CONSUMING DELICIOUS MEATS.." - try { do-build $args } catch { print "FAILED TO CONSUME MEATS." } - } -} - -def cmd-cook [...args: string] { - with-frame { - meat-print "PREPARING DELICIOUS MEATS.." - try { ^nix build --no-link (flake-target) ...$args } - } -} - -def cmd-poke [...args: string] { - with-frame { - meat-print "PREPARING SUSPICIOUS MEATS.." - try { ^nix build --no-link (flake-target) --show-trace ...$args } - } -} - -def cmd-gut [...args: string] { - with-frame { - meat-print "CLEANING MEAT STORES.." - try { ^nh clean all ...$args } - } -} - -def cmd-trade [] { - with-frame { - meat-print "TRADING FOREIGN MEATS.." - meat-print "tbd" - } -} - -def cmd-fresh [...flakes: string] { - with-frame { - meat-print "HUNTING FRESH MEATS.." - let root = $env.MEATS - if ($flakes | is-empty) { - try { ^nix flake update --flake $root } - } else { - for f in $flakes { - if ($f | str downcase) == "meat" { - meat-print "PROCESSING REAL MEAT.." - } else { - meat-print $"PROCESSING FRESH MEAT ($f | str upcase).." - } - try { ^nix flake update $f --flake $root } - } - } - } - print "" -} - -def lookup-input [nodes: record, name: string, node_ref: any] { - let node_name = if (($node_ref | describe) == "string") { $node_ref } else { $name } - let node = $nodes | get $node_name - let locked = $node.locked - let original = $node.original - let url = match $locked.type { - "github" => $"https://github.com/($locked.owner)/($locked.repo).git" - "git" => $original.url - _ => { error make { msg: $"unsupported type: ($locked.type)" } } - } - let target_ref = if ('ref' in $original) { $"refs/heads/($original.ref)" } else { "HEAD" } - let out = ^git ls-remote $url $target_ref | complete - if $out.exit_code != 0 { - error make { msg: "git ls-remote failed" } - } - let first_line = $out.stdout | str trim | lines | get 0? - if ($first_line | is-empty) { - error make { msg: "no refs returned" } - } - let latest = $first_line | split row "\t" | first - { name: $name, locked: $locked.rev, latest: $latest } -} - -def cmd-look [] { - with-frame { - meat-print "LOOK FOR NEW MEATS.." - let lock = open --raw $"($env.MEATS)/flake.lock" | from json - let nodes = $lock.nodes - let root_inputs = $nodes.root.inputs - let stale = $root_inputs | transpose name node_ref | each { |row| - try { - let r = lookup-input $nodes $row.name $row.node_ref - if $r.latest != $r.locked { - meat-print $"($r.name | str upcase) MEAT RIPE!" - $r.name - } else { null } - } catch { - meat-print $"NO FIND ($row.name | str upcase).." - null - } - } | compact - if ($stale | is-empty) { - meat-print "NO MEAT FRESHER" - } - } -} - -def truncate-desc [s: string, n: int]: nothing -> string { - if (($s | str length) > $n) { - ($s | str substring 0..$n) + ".." - } else { - $s - } -} - -def cmd-hunt [...query: string] { - with-frame { - if ($query | is-empty) { - meat-print "WHAT MEAT YOU SEEK?" - return - } - let q = $query | str join " " - meat-print $"HUNTING FOR ($q | str upcase).." - let body = { - from: 0, - size: 20, - sort: [{ _score: "desc", package_attr_name: "desc", package_pversion: "desc" }], - collapse: { field: "package_attr_name" }, - query: { - bool: { - must: [ - { term: { type: "package" } }, - { multi_match: { - type: "cross_fields", - query: $q, - fields: ["package_attr_name^9", "package_pname^6", "package_description^1.3", "package_longDescription^1"] - } } - ] - } - } - } - try { - let resp = ( - http post - --user "aWVSALXpZv" - --password "X8gPHnzL52wFEekuxsfQ9cSh" - --content-type "application/json" - "https://nixos-search-7-1733963800.us-east-1.bonsaisearch.net/nixos-*-unstable-*/_search" - $body - ) - let hits = $resp.hits?.hits? | default [] - if ($hits | is-empty) { - meat-print "NO MEATS FOUND!" - } else { - for hit in $hits { - let src = $hit._source - let name = $src.package_attr_name? | default "?" - let ver = $src.package_pversion? | default "" - let desc = $src.package_description? | default "" - let ver_str = if ($ver | is-empty) { "" } else { $" \(($ver)\)" } - print $" \tnixpkgs#($name)($ver_str)" - if not ($desc | is-empty) { - print $" \t (truncate-desc $desc 60)" - } - } - } - } catch { |e| - meat-print $"HUNT FAILED: ($e.msg)" - } - } -} - -def cmd-ritual [] { - with-frame { - meat-print "PREPARING RITUAL GROUND.." - let tmpdir = ^mktemp -d -t "meat-chew.XXXXXX" | str trim - let meats = $env.MEATS - let nix_conf = $"($tmpdir)/nix.conf" - let build = $"($tmpdir)/build" - try { - nix-build-nom $nix_conf $"($meats)/entry.nix" (nix-conf-attr) - meat-print "CONSUMING MEATS.." - with-env { NIX_USER_CONF_FILES: $nix_conf } { - nix-build-nom $build $"($meats)/entry.nix" (system-attr) - } - ^$env.DIFFER /nix/var/nix/profiles/system $build - activate $build - try { rm $nix_conf } - try { rm $build } - } catch { - print "FAILED TO CONSUME MEATS." - } - } -} - -def cmd-shelter [new_closure: string] { - activate $new_closure -} - -def main [...args: string] { - if ($env.MEATS? | is-empty) { - meat-print "NO PATH TO RUNESTONE FOUND!" - cmd-help - return - } - let sub = $args | get 0? | default "" | str downcase - let rest = $args | skip 1 - match $sub { - "yum" => { cmd-yum ...$rest } - "cook" => { cmd-cook ...$rest } - "poke" => { cmd-poke ...$rest } - "gut" => { cmd-gut ...$rest } - "trade" => { cmd-trade } - "look" => { cmd-look } - "fresh" => { cmd-fresh ...$rest } - "hunt" => { cmd-hunt ...$rest } - "ritual" => { cmd-ritual } - "shelter" => { cmd-shelter $rest } - _ => { cmd-help } - } -} diff --git a/meat.opam b/meat.opam new file mode 100644 index 0000000..f89d30f --- /dev/null +++ b/meat.opam @@ -0,0 +1,31 @@ +# This file is generated by dune, edit dune-project instead +opam-version: "2.0" +synopsis: "A short synopsis" +description: "A longer description" +maintainer: ["Maintainer Name "] +authors: ["Author Name "] +license: "LICENSE" +tags: ["add topics" "to describe" "your" "project"] +homepage: "https://github.com/username/reponame" +doc: "https://url/to/documentation" +bug-reports: "https://github.com/username/reponame/issues" +depends: [ + "ocaml" + "dune" {>= "3.16"} + "odoc" {with-doc} +] +build: [ + ["dune" "subst"] {dev} + [ + "dune" + "build" + "-p" + name + "-j" + jobs + "@install" + "@runtest" {with-test} + "@doc" {with-doc} + ] +] +dev-repo: "git+https://github.com/username/reponame.git" diff --git a/nix/default.nix b/nix/default.nix index 2215000..a6004a4 100644 --- a/nix/default.nix +++ b/nix/default.nix @@ -1,39 +1,17 @@ { - lib, - stdenvNoCC, - nushell, - makeBinaryWrapper, + ocamlPackages, + buildInputs, + git, version, - differ, - monitor, ... }: -stdenvNoCC.mkDerivation { +ocamlPackages.buildDunePackage { pname = "meat"; version = "0.1-delicious-${version}"; + minimalOCamlVersion = "5.2"; + src = ./..; - - nativeBuildInputs = [ makeBinaryWrapper ]; - - dontBuild = true; - - installPhase = '' - runHook preInstall - install -Dm644 meat.nu $out/share/meat/meat.nu - mkdir -p $out/bin - makeWrapper ${nushell}/bin/nu $out/bin/meat \ - --add-flags "$out/share/meat/meat.nu" \ - --set DIFFER ${ - lib.makeBinPath [ - differ - ] - } - --set MONITOR ${ - lib.makeBinPath [ - monitor - ] - } - runHook postInstall - ''; + nativeBuildInputs = [ git ]; + buildInputs = buildInputs; } diff --git a/nix/module.nix b/nix/module.nix index b2d6079..a45e9ef 100644 --- a/nix/module.nix +++ b/nix/module.nix @@ -19,21 +19,14 @@ in type = types.package; description = "your ideal meat"; }; - differ = mkOption { - type = types.package; - description = "diffing tool to use"; - default = pkgs.dix; - }; - monitor = mkOption { - type = types.package; - description = "nix monitoring tool to use"; - default = pkgs.nix-output-monitor; - }; }; config = lib.mkIf cfg.enable { environment.sessionVariables.MEATS = cfg.flake; environment.systemPackages = [ cfg.package + pkgs.nh # for now.. + pkgs.nix-output-monitor + pkgs.dix ]; }; } diff --git a/test/dune b/test/dune new file mode 100644 index 0000000..249a645 --- /dev/null +++ b/test/dune @@ -0,0 +1,2 @@ +(test + (name test_meat)) diff --git a/test/test_meat.ml b/test/test_meat.ml new file mode 100644 index 0000000..e69de29