Skip to content

Instantly share code, notes, and snippets.

@lf94

lf94/.sml Secret

Last active September 12, 2022 14:02
Show Gist options
  • Save lf94/d16b55fa7c79d989e211367a44d1a9cc to your computer and use it in GitHub Desktop.
Save lf94/d16b55fa7c79d989e211367a44d1a9cc to your computer and use it in GitHub Desktop.
(*------------------------------------------------------------------------------
REPLICARE
recipere + deplicare
"to receive" + "explain"
Write SML programs to output shell code for installations and deployments.
Modify as needed.
--------------------------------------------------------------------------------
Why not just use shell scripts directly?
The lack of a type-system in all shell scripting languages is, when you think
about, horrific. It takes one bad input to destroy a system. With types this can
lift the level of safety very high, as now the script will not run before it is
considered correct. In hindsight it will be clear that all scripting languages
should have had strong typing from the beginning.
There is not much more to say than that. Hindley-Milner type systems allow a
great level of type inference, which means the developer of scripts does not
need to type everything. It's this gradient of typing which suits a whole
spectrum of needs that is powerful.
--------------------------------------------------------------------------------
Utilities *)
val join = String.concatWith;
fun cp (programInvocation:string) (srcs:string list) (dest:string)
= join " " [programInvocation, join " " srcs, dest]
;
datatype State = Exists | Fresh
datatype OSObject = User of string | Directory of string | Software of string;
datatype Location
= Local of string list
| Remote of string list
;
(* Change this to a prefered copy program, i.e. scp *)
fun rsync (port:string) = cp (String.concat ["rsync -vv -e 'ssh -p ", port, "'"]);
(* Change this to the package manager on the remote system, i.e. pacman *)
structure PackageManager =
struct
val pkgman = "apt"
fun install software = String.concat [pkgman, " install -y ", software];
end
fun createSession (user, host, port)
= let
fun remote cmd
= String.concat ["ssh -p ", port, " ", user, "@", host, " -C '", cmd, "'"]
;
fun copy (Local from) (Remote to)
= rsync port from ("'" ^ user ^ "@" ^ host ^ ":" ^ (join " " to) ^ "'")
| copy (Remote from) (Local to)
= rsync port ["'" ^ user ^ "@" ^ host ^ ":" ^ (join " " from) ^ "'"] (List.nth (to, 0))
| copy _ _
= ""
;
fun ensure (User user) Exists
= (remote o String.concat) ["id ", user, " || useradd -m ", user]
| ensure (Directory dir) Exists
= (remote o String.concat) ["[ -d \"", dir, "\" ] || mkdir -p ", dir]
| ensure (Directory dir) Fresh
= (remote o String.concat) ["rm -rf ", dir, " && mkdir -p ", dir]
| ensure (Software ware) Exists
= (remote o String.concat) ["which ", ware, " || ", PackageManager.install ware]
| ensure (Software ware) Fresh
= (remote o String.concat) [PackageManager.install ware]
| ensure _ _
= ""
;
in (copy, ensure, remote)
end;
fun each statements var path =
String.concat [
String.concat ["for ", var, " in ", path, "; do "],
join "; " statements,
"; done"
];
fun shell cmd = cmd;
fun exec cmds = print (join " && " cmds);
(*------------------------------------------------------------------------------
Example: deploy blog to a host *)
val target = ("root", "len.falken.directory", "9999");
val (copy, ensure, remote) = createSession target;
exec [
ensure (User "len") Exists,
ensure (Directory "/home/len/www/") Exists,
ensure (Software "rsync") Exists,
shell "/home/len/Wiki/rss.sh > /home/len/Wiki/feed.xml",
copy (Local ["/home/len/Wiki/public", "/home/len/Wiki/feed.xml", "/home/len/Wiki/index.css"])
(Remote ["/home/len/www/"])
]
(*------------------------------------------------------------------------------
Example: self-host git projects *)
val cwd = "/home/len/Code/len/git-self-host";
exec [
ensure (User "len") Exists,
ensure (Software "rsync") Exists,
ensure (Software "git") Exists,
shell String.concat [cwd, "/rss.sh > ", cwd, "/code.xml"],
copy (Local [cwd ^ "/code.xml", cwd ^ "/index.css"])
(Remote ["/home/len/www/"]),
each [
copy (Local ["$file/.git/"])
(Remote ["/home/len/www/$(basename $file).git"]),
remote "cd /home/len/www/$(basename $file).git/ && git config --bool core.bare true"
] "file" (cwd ^ "/../*")
]
(*
TODO: Use configuration templates for things like nginx
This will be simple: copy (Local [template "templates/nginx.conf" [["key","value"]]]) (Remote ["some place"])
template will return a path to a temporary file
It will invoke whatever templating tech you'd like.
*)
@jcf
Copy link

jcf commented Sep 11, 2022

Interesting idea. This reminded me of the discomfort caused by my use of Ansible and got the grey matter tingling.

Interestingly, your revision history demonstrates the limited impact of applying types to this problem.

s/lee/len

Both versions type check; only one is correct. Until you can type the behaviour of the outside world, this has limited utility (and with this incantation, a Nix maximalist is summoned forth from the digital ether).

Thank you for sharing!

@lf94
Copy link
Author

lf94 commented Sep 11, 2022

Hm, I'm not sure this example is useful to bring up, or anything that requires to "know" your targets... Let's say that was a typo - this could in fact be encoded in the program to be prevented. The idea I'm trying to convey here is "type as much as you need, abstract as much as you need". Standard ML doesn't support value literal types like TypeScript so it can't be done as elegantly at the type system level, but if you really wanted to, you could do:

datatype Username = Len | Lee | Leo | Lem;
ensure (User Len) Exists;

And then you can expand those to strings throughout the code...!

There are a few alternatives also I can think of, but you get the idea.

FWIW TypeScript would work just as well. I prefer Standard ML these days for personal code: http://len.falken.directory/p-lang/100-year-programs.txt

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment