DkCoder: Scripting at Scale
Hello Builder! Scripting is a small, free and important piece of DkSDK.
A few clicks from your web browser and four (4) minutes later you and your Windows and macOS users can start scripting with DkCoder. And all users, including glibc-based Linux desktop users, can use their Unix shells or Windows PowerShell. Nothing needs to be pre-installed on Windows and macOS. Just copy and paste two lines (you’ll see examples soon) and your script is running and your project is editable with an LSP-capable IDE like Visual Studio Code.
Unlike most scripting frameworks, DkCoder solves the problem of scale: you start with small scripts that do immediately useful things for you and your team, and when inevitably you need to expand, distribute or embed those scripts to make full-featured applications, you don’t need to throw out what you have already written. DkCoder is a re-imagining of the scripting experience that re-uses the best historical ideas:
- You don’t write build files. If that sounds like Unix
/bin/sh
or the Windows Command Prompt, that is intentional. - Most files you write can be immediately run. If that sounds like how Python scripts are almost indistinguishable from Python modules, or like JavaScript modules, that is intentional.
- Most files you write can be referenced with a fully-qualified name. If that sounds like Java packages and how that has been proven to scale to large code bases, that is intentional.
- Your scripts play well together and don’t bit rot. It is conventional to add static typing (Typescript, mypy) when scripting projects get large. DkCoder has type-safety from Day One that is safer and easier to use.
You’ll start with the mouse-click install and the basic one-liner:
let () = Tr1Stdlib_V414Io.StdIo.print_endline "Hello Builder!"
which you run with a single command:
./dk DkRun_V2_2.Run -- DkHelloScript_Std.AndHello
Hello Builder!
You’ll do a quick tour of the prior art where we will acknowledge situations when DkCoder is not the right tool for you.
You’ll see three examples of scripts:
- the integration test script that produces this documentation page
- a 2D game to show non-traditional uses of scripts and the re-use of existing code
- the security-conscious production service managing subscriptions for DkSDK Pricing
Along the way you’ll encounter a small language made by a community who can write some very good libraries. And a software kit that gets out the way and makes good software accessible.
Let’s begin!
You, your co-developers and your users can start scripting in a couple clicks.
FIRST, if you don't have these installed then install Git from https://git-scm.com/downloads and Visual Studio Code from https://code.visualstudio.com/download.
Then click this link: Clone and Open DkHelloScript in Visual Studio Code.
SECOND, open src/DkHelloScript_Std/AndHello.ml
in your IDE or open src/DkHelloScript_Std/AndHello.ml in your browser.
You should see:
open Tr1Stdlib_V414Io
let () = StdIo.print_endline "Hello Builder!"
THIRD,
open a Terminal > New Terminal
and run:
./dk DkRun_V2_2.Run -- DkHelloScript_Std.AndHello
Hello Builder!
✔️ DONE! Ok, one-liners are not very interesting.
But ... that should have only taken a few minutes.
And ... the same command works on Windows, macOS and GNU/Linux.
Reproducibility or quick typing? Pick one
We specified a version number and a double dash separator (DkRun_V2_2.Run --
) in our last example:
./dk DkRun_V2_2.Run -- DkHelloScript_Std.AndHello
You can relax your fingers by instead typing:
./dk DkHelloScript_Std.AndHello
In your everyday scripting you won’t want to type DkRun_V2_2.Run --
.
Leave it out.
However, when you are publishing documentation (like this article) you should
always include the version number. You’ll find it easy for your users
to copy-and-paste and more importantly, the behavior of your scripts won’t
change in some future version of
./dk
.
Focus on what you are running
Open src/DkHelloScript_Std/AndHelloAgain.ml
in your IDE.
It has the same content as DkHelloScript_Std/AndHello.ml
.
However, it has red squiggly lines.
The red is an indication that DkCoder has not compiled the script. That is a good thing because generally you don't want to waste time compiling every script every time you make an edit in a large project.
Now run the DkHelloScript_Std.AndHelloAgain
script with:
./dk DkRun_V2_2.Run -- DkHelloScript_Std.AndHelloAgain
Visual Studio Code should remove the red in a minute.
DkCoder optimizes for rapid iterative development by:
- Only compiling the script that you last ran (ex.
DkHelloScript_Std/AndHelloAgain.ml
). If your script requires other scripts to run, those are also compiled. - Compiling all the scripts in a project when you first open Visual Studio Code. That means you can browse your project when you first start your day without seeing any red. Then, when you have found a script you want to edit or a new script to add, edit and run that script repeatedly throughout the day.
Runtime requirements
-
Windows 10 (version 1903 aka 19H1, or later), or Windows 11, either 32-bit or 64-bit.
-
macOS AMD/Intel or Apple Silicon (Big Sur 11.0 or higher).
-
Linux AMD/Intel 64-bit with glibc 2.27 or higher.
-
You will need the following system tools installed:
tar
,wget
orcurl
,git
,unzip
libssl-dev
(Debian, Ubunutu) oropenssl-devel
(RedHat)ninja
(this requirement will be removed)- If any are missing you will be asked if DkCoder can install them the first time you run
./dk
from the command line.
-
For graphics you will need one of:
- X11 with a direct OpenGL driver.
- A good prereq test is running
glxgears
(ex.yum install glx-utils; glxgears
). If that works, DkCoder graphics should work. - Don’t expect the
LIBGL_ALWAYS_INDIRECT=1
environment variable to work. That is sometimes recommended when forwarding X11 withssh -X
or into a Docker container, but it forces the protocol to be a too-old OpenGL version 1.4.
- A good prereq test is running
- Wayland. Set the environment variable
SDL_VIDEODRIVER=wayland
to force it. - Linux Direct Rendering Manager. If supported, you will have a
/dev/dri/card0
. Set the environment variableSDL_VIDEODRIVER=kmsdrm
to force it.
- X11 with a direct OpenGL driver.
-
That’s it. Linux desktops are complex!
-
Runtime system
Your scripts will have access to the following libraries:
- curl
- All OSes:
- Brotli and zstd compression
- Asynchronous DNS resolution
- HTTP/2
- Websockets
- Windows: Schannel TLS backend (Microsoft CryptoAPI) and WinIDN domain name resolution
- macOS: Secure transport backend (Keychain) and IDN2
- Linux: Openssl is required from the operating system. DkCoder does not provide it.
- All OSes:
- SDL2
Basic shell scripting
In an earlier section an OCaml programming language environment was transparently downloaded for you. You will be using OCaml in this walkthrough, even though you may find other DkSDK documentation that uses DkCoder with C and Java. You can proceed through this walkthrough without knowing OCaml.
In this section we’ll be using the shexp library that was created by Jane Street Capital. If you are familiar with traditional shell scripts like bash
you’ll find shexp
more powerful.
Open src/DkHelloScript_Std/B43Shell/B35Shexp/B43Countdown.ml
in your IDE or open src/DkHelloScript_Std/B43Shell/B35Shexp/B43Countdown.ml in your browser.
You should see:
open Tr1Shexp_Std.Shexp_process
open Tr1Shexp_Std.BindsShexp
(* Counts down from n with a one second delay between ticks *)
let rec countdown (n : int) : unit t =
if n > 0 then begin
echo (string_of_int n) ;%bind
sleep 1.0 ;%bind
countdown (n - 1)
end
else echo "Done countdown. Bye Builder!"
let main_t : unit t =
echo "Hello Builder! ..." ;%bind
echo "------------------------------------------------------------" ;%bind
echo "Starting countdown..." ;%bind
countdown 5
let () = if Tr1EntryName.module_id = __MODULE_ID__ then eval main_t
And running it with:
./dk DkRun_V2_2.Run -- DkHelloScript_Std.B43Shell.B35Shexp.B43Countdown
gives:
Hello Builder! ...
------------------------------------------------------------
Starting countdown...
5
4
3
2
1
Done countdown. Bye Builder!
Prior Art
This is only a quick tour of the most popular alternatives to DkCoder. Most of the advantages for DkCoder boil down to it works on Windows and Unix without pre-installing other packages.
- Shell scripts like
/bin/bash
. A POSIX-compatible shell script is pre-installed on almost all Unix distributions. By comparison, DkCoder has a POSIX shell script launcher that transparently installs pre-compiled binaries for macOS and the major Linux desktop distributions. However, DkCoder has expanded reach with its Windows shell script launcher that transparently installs on Windows (and Unix) machines. - PowerShell. The scripts are written in a full-featured programming
language, and in that respect PowerShell is similar to DkCoder. Furthermore,
PowerShell has an command shell that is a complete alternative to both the
Command Prompt on Windows and
/bin/sh
on Unix. DkCoder does not have a command shell. However, DkCoder scripts work on both Windows and Unix without pre-installing the PowerShell distribution, so DkCoder may be easier to adopt. - Perl.
CPAN
gives Perl a huge standardized package registry with standardized package tools. By comparison, DkCoder has no packages (today!) in its package registry. However, DkCoder will soon have an alternative to package registries that is expected to be the main form of sharing until DkCoder is out of its infancy stage. Also, as with shell scripts, Perl is pre-installed on almost all Unix distributions. However, DkCoder has expanded reach with its Windows and Unix shell script launcher. - Python. Most of the advantages and disadvantages for Perl apply to Python. Python is less ubiquitous but more popular than Perl. DkCoder scripting should have a similar learning curve to Python, as evidenced by its early use by high-schoolers.
- Fortran. The DkCoder author (me!) has never written a line of Fortran, but its “dynamic dependency” system of Fortran files containing declarations of which Fortran modules are used is similar to DkCoder. DkCoder goes a step further and drops the need to declare dependencies that are available in the DkRegistry.
- etc.
There are a few reasons not to use DkCoder:
- OCaml has memory inefficiences due to garbage collection, boxing, cache non-locality and wide native types. This would affect you if your application is memory-constrained rather than CPU or IO-constrained. The main mitigation is that using OCaml FFI to interface with memory efficient languages like C and Rust is not overly complex.
- The OCaml ecosystem is small and biased towards 64-bit Linux applications. This would
affect you if you develop applications on Windows or for mobile devices.
The main mitigation is to use cross-platform distributions like
esy
, DkML and DkSDK. - The company behind DkCoder is very small (at times only one FTE). In particular, some
features like fast native compilation are going to take substantial time to deliver.
The main mitigation is that DkSDK customers get source code rights for life, and can prioritize their wishlist with sponsored feature development. - DkCoder is currently in Alpha. Major features like auto-downloading of other scripts are almost feature complete but paused while we collect Alpha feedback. The main mitigation is to wait if you need post-Alpha features.
- The licensing of OCaml (LGPL) and the broader OCaml ecosystem (many GPL) may be too restrictive for some companies to adopt, especially in comparison to many modern (Rust, Go) or dominant (C, Python, JavaScript) ecosystems. The main mitigation is to strategically choose either static and shared linking (tools in DkSDK CMake and DkSDK FFI C make this easier).
Integration Testing
Let’s run a script that is simultaneously both an integration test and source of documentation:
./dk DkRun_V2_2.Run DkHelloScript_Std.Y33Article --serve
While that is running, open your web browser to http://localhost:8080
You will see a preview of the documentation you are reading right now! The documentation you are reading is a side-effect of running integration tests.
Open src/DkHelloScript_Std/Y33Article.ml
in your IDE or open src/DkHelloScript_Std/Y33Article.ml in your browser.
You should see:
open Tr1Tezt_C.Tezt
open Tr1Htmlit_Std.Htmlit
(** {1 Register Tests incl. Documentation and Server}
If you are familiar with React, you can "push down" functions and values
so that your deeper content can render itself. We do the same thing
here, and that is why you see all the [~make_capture ~run_dk] and other
parameters.
We could have also used a `src/DkHelloScript_Std/open__.ml` file
to provide values to all the scripts of the [DkHelloScript_Std] library.
If you are familiar with React, that "global" style of distributing
values would be called a "context".
Even simpler would be to create functions inside [Doc] since
[Y33ArticleX/Doc] is visible to the [Y33ArticleX/Section*] scripts.
If possible find a module like [Doc] that is visible to all the scripts
that need it, and place your functions in the module. We don't do it
in this project except for one function {!ucodeblock} for demonstration
purposes.
If that doesn't work for the way your have organized your project code,
prefer the "push down" method over the "global context". It is simpler
to test, your functions will be re-usable outside of the current
library (ex. outside of [DkHelloScript_Std]), and you won't pull in
unnecessary dependencies for scripts that don't need it.
You can avoid "unused parameters" errors with the "push down" method
by attaching the following extension on your "push down" functions:
[[
let register_before_tests (* ... *) =
(* ... *)
unit
[@@warning "-unused-var-strict"]
]] *)
let title_section () =
Y33ArticleX.Doc.(
append
(usection
[ ucontainer
El.
[p ~at:[At.class' "subtitle"] [txt "DkCoder: Scripting at Scale"]]
] ) )
let () =
if Tr1EntryName.module_id = __MODULE_ID__ then begin
(* SECTIONS *)
let andhello_file = AndHello.__FILE__ in
let andhello_module = AndHello.__MODULE_ID__ in
title_section () ;
Y33ArticleX.S004Intro.register_before_tests ~andhello_module () ;
Y33ArticleX.S008StartScript.register_before_tests ~andhello_module
~andhello_file () ;
Y33ArticleX.S012Reproducib.register_before_tests ~andhello_module () ;
Y33ArticleX.S016Focus.register_before_tests ~andhello_file
~andhelloagain_module:AndHelloAgain.__MODULE_ID__
~andhelloagain_file:AndHelloAgain.__FILE__ () ;
Y33ArticleX.S020RuntimeReqs.register_before_tests () ;
Y33ArticleX.S022RuntimeSys.register_before_tests () ;
Y33ArticleX.S024BasShell.register_before_tests
~shexpcountdown_module:B43Shell.Index.B35Shexp.B43Countdown.__MODULE_ID__
~shexpcountdown_file:B43Shell.Index.B35Shexp.B43Countdown.__FILE__ () ;
Y33ArticleX.S028PriorArt.register_before_tests () ;
Y33ArticleX.S032Testing.register_before_tests ~article_module:__MODULE_ID__
~article_file:__FILE__ () ;
Y33ArticleX.S036FlairGraph.register_before_tests
~boguetiny_module:B57Graphics.Index.B43Bogue.B43Tiny.__MODULE_ID__
~boguetiny_file:B57Graphics.Index.B43Bogue.B43Tiny.__FILE__ () ;
Y33ArticleX.S038SnokeGame.register_before_tests () ;
(* Y33ArticleX.S040ReusingYouScripts.register_before_tests () ; *)
(* Y33ArticleX.S044ListingThirdParties.register_before_tests () ; *)
(* Y33ArticleX.S048UsingListedThirdParties.register_before_tests () ; *)
(* Y33ArticleX.S052FunBreak.register_before_tests () ; *)
(* Y33ArticleX.S056UsingRegThirdParties.register_before_tests () ; *)
(* Y33ArticleX.S060DkRegistry.register_before_tests () ; *)
Y33ArticleX.S064MakeProject.register_before_tests () ;
Y33ArticleX.S066Production.register_before_tests () ;
Y33ArticleX.S068Parties.register_before_tests () ;
Y33ArticleX.S072Stdlib.register_before_tests () ;
Y33ArticleX.S076RuntimeLibs.register_before_tests () ;
Y33ArticleX.S080InducLimits.register_before_tests () ;
Y33ArticleX.S084EarlyLimits.register_before_tests () ;
Y33ArticleX.S088SecDesign.register_before_tests () ;
Y33ArticleX.S092InTouch.register_before_tests () ;
(* HTTP SERVER *)
Y33ArticleX.Httpd.register_before_tests () ;
(* DOCUMENTATION PRINTING *)
Y33ArticleX.Doc.register_before_tests () ;
(* RUN TESTS *)
Test.run ()
end
The test infrastructure is provided by the module Tr1Tezt_C.Tezt
. I recommend you read the Announcing Tezt by Nomadic Labs article to see what it can do and why it was developed.
I wrote the module DkHelloScript_Std.Y33ArticleX.Doc
in a very “scripty” style: it incrementally builds documentation in memory. It renders to either HTML or Markdown, and makes use of the Bulma CSS Framework for styling and layout. You can copy and customize the module in your own scripts. For those of you familiar with JavaScript and the DOM, the incremental approach is similar to how your web browser renders a web page. For everybody else, just think of a file being created in-memory the first time DkHelloScript_Std.Y33ArticleX.Doc
is accessed, and through the use of several helper functions that write (“append”) fragments at the end of the file, the DkHelloScript_Std.Y33ArticleX.Doc.register_before_tests ()
is able to print a complete documentation file to the console or a real file.
The title_section ()
function is the first documentation fragment that is built. You should recognize it on the top of this documentation page.
Each section of the documentation has its own test script which registers its own integration test.
Once all of the tests are registered, they are all run with the OCaml function call:
Test.run ()
Let’s drill down into this section of the documentation.
Open src/DkHelloScript_Std/Y33ArticleX/S032Testing.ml
in your IDE or open src/DkHelloScript_Std/Y33ArticleX/S032Testing.ml in your browser.
open Tr1Tezt_C.Tezt
open Tr1Tezt_C.Tezt.Base
open Tr1Htmlit_Std.Htmlit
module Printf = Tr1Stdlib_V414CRuntime.Printf
let register_before_tests ~article_module ~article_file () =
Test.register ~__FILE__ ~title:"testing" ~tags:[]
@@ fun () ->
let self_FILE = __FILE__ in
let open Doc in
let open El in
append
(usection
[ ucontainer
[ ucard ~title:"Integration Testing" []
; upar
[ txt
"Let's run a script that is simultaneously both an \
integration test and source of documentation:" ]
; ucodeblock `Shell
(Printf.sprintf "./dk %s %s --serve" Tr1Version.run_module
article_module )
; upar
[ txt "While that is running, open your web browser to "
; ulink ~url:"http://localhost:8080" "http://localhost:8080" ]
; upar
[ txt
"You will see a preview of the documentation you are \
reading right now! "
; unsafe_raw
"<strong>The documentation you are reading is a side-effect \
of running integration tests.</strong>" ]
; ucodeaction ~ide:(txt "Open") ~browser:(Some "open") article_file
; txt "You should see:"
; ucodefile article_file
; upar
[ txt "The test infrastructure is provided by the module "
; ucode "Tr1Tezt_C.Tezt"
; txt ". I recommend you read the "
; ulink
~url:
"https://research-development.nomadic-labs.com/announcing-tezt.html"
"Announcing Tezt by Nomadic Labs"
; txt " article to see what it can do and why it was developed. "
]
; uinfo
[ txt
"DkCoder has Tezt pre-installed, so you won't have to \
create 'dune' files or do any 'dune build' steps that \
regular OCaml developers would have to do. To use Tezt, \
follow the examples that are given to you in this \
documentation." ]
; upar
[ txt "I wrote the module "
; ucode Doc.__MODULE_ID__
; txt
" in a very \"scripty\" style: it incrementally builds \
documentation in memory. "
; txt
"It renders to either HTML or Markdown, and makes use of \
the "
; ulink ~url:"https://bulma.io/" "Bulma CSS Framework"
; txt " for styling and layout. "
; txt
"You can copy and customize the module in your own scripts. "
; txt "For those of you familiar with "
; ulink
~url:
"https://developer.mozilla.org/en-US/docs/Web/API/Document_Object_Model/Introduction"
"JavaScript and the DOM"
; txt
", the incremental approach is similar to how your web \
browser renders a web page. For everybody else, just think \
of a file being created in-memory the first time "
; ucode Doc.__MODULE_ID__
; txt
" is accessed, and through the use of several helper \
functions that write (\"append\") fragments at the end of \
the file, the "
; ucode
(Printf.sprintf "%s.register_before_tests ()"
Doc.__MODULE_ID__ )
; txt
" is able to print a complete documentation file to the \
console or a real file." ]
; upar
[ txt "The "
; ucode "title_section ()"
; txt
" function is the first documentation fragment that is \
built. "
; txt
"You should recognize it on the top of this documentation \
page." ]
; uinfo
~header:[txt "Imperative programming"]
[ txt "You may have heard that OCaml is a functional language. "
; txt
"However, OCaml also supports imperative programming with \
global variables. "
; txt
"The Tezt test framework uses imperative programming to \
allow you to register tests whenever you want. "
; txt
"I personally find imperative programming the most natural \
programming model when writing scripts. " ]
; upar
[ txt
"Each section of the documentation has its own test script \
which registers its own integration test. " ]
; upar
[ txt
"Once all of the tests are registered, they are all run \
with the OCaml function call:"
; ucodeblock `OCaml "Test.run ()" ]
; upar
[txt "Let's drill down into this section of the documentation. "]
; ucodeaction ~ide:(txt "Open") ~browser:(Some "open") self_FILE
; ucodefile self_FILE
; upar
[ txt "You will see some brief boilerplate to register the test ("
; ucode "Test.register ~__FILE__ ..."
; txt
"). The test adds a \"section\" fragment to the in-memory \
documentation. In fact, the side-effect of creating the \
section fragment is all that the test does." ]
; upar [txt "Let's see something more complex."]
; ucodeaction
~ide:(txt "Open the \"Basic shell scripting\" test")
~browser:(Some "open") S024BasShell.__FILE__
; ucodefile S024BasShell.__FILE__
; upar
[ txt "In this test we run a script from the command line ("
; ucode "RunDk.make_capture ()"
; txt " and "
; ucode "RunDk.run_dk ~hooks [shexpcountdown_module]"
; txt "). "
; txt
"The source code, the script command and its output become \
part of the documentation:"
; ucodeblock `OCaml
{|
; ucodefile shexpcountdown_file
; ucodeblock `Shell !ref_cmd
; ucodeblock `Output dk_output
|}
] ] ] ) ;
unit
[@@warning "-unused-var-strict"]
You will see some brief boilerplate to register the test (Test.register ~__FILE__ ...
). The test adds a “section” fragment to the in-memory documentation. In fact, the side-effect of creating the section fragment is all that the test does.
Let’s see something more complex.
Open the “Basic shell scripting” test src/DkHelloScript_Std/Y33ArticleX/S024BasShell.ml
in your IDE or open src/DkHelloScript_Std/Y33ArticleX/S024BasShell.ml in your browser.
open Tr1Tezt_C.Tezt
open Tr1Tezt_C.Tezt.Base
open Tr1Htmlit_Std.Htmlit
let register_before_tests ~shexpcountdown_module ~shexpcountdown_file () =
Test.register ~__FILE__ ~title:"basic shell scripting" ~tags:[]
@@ fun () ->
let ref_cmd, hooks = RunDk.make_capture () in
let* dk_output = RunDk.run_dk ~hooks [shexpcountdown_module] in
let open Doc in
let open El in
append
(usection
[ ucontainer
[ ucard ~title:"Basic shell scripting" []
; upar
[ unsafe_raw
"In an earlier section an <strong>OCaml</strong> \
programming language environment was transparently \
downloaded for you. "
; txt
"You will be using OCaml in this walkthrough, even though \
you may find other DkSDK documentation that uses DkCoder \
with C and Java. "
; txt
"You can proceed through this walkthrough without knowing \
OCaml." ]
; uinfo
[ unsafe_raw
{|Once you are done the DkCoder walkthrough you may want to go to the <a href="https://ocaml.org/docs">OCaml - Learn</a> site. |}
; unsafe_raw
{|For this walkthrough <strong>we'll stick to ordinary OCaml open-source examples</strong> that you can replicate in a conventional OCaml environment.|}
]
; upar
[ txt "In this section we'll be using the "
; ulink ~url:"https://github.com/janestreet/shexp" "shexp"
; txt " library that was created by "
; ulink
~url:"https://www.janestreet.com/join-jane-street/overview/"
"Jane Street Capital"
; txt
". If you are familiar with traditional shell scripts like "
; ucode "bash"
; txt " you'll find "
; ucode "shexp"
; txt " more powerful." ]
; upar
[ ucodeaction ~ide:(txt "Open") ~browser:(Some "open")
shexpcountdown_file
; txt "You should see:" ]
; ucodefile shexpcountdown_file
; upar [txt "And running it with:"]
; ucodeblock `Shell !ref_cmd
; txt "gives:"
; ucodeblock `Output dk_output ] ] ) ;
unit
[@@warning "-unused-var-strict"]
In this test we run a script from the command line (RunDk.make_capture ()
and RunDk.run_dk ~hooks [shexpcountdown_module]
). The source code, the script command and its output become part of the documentation:
; ucodefile shexpcountdown_file
; ucodeblock `Shell !ref_cmd
; ucodeblock `Output dk_output
Adding some flair with graphics
Open src/DkHelloScript_Std/B57Graphics/B43Bogue/B43Tiny.ml
in your IDE or open src/DkHelloScript_Std/B57Graphics/B43Bogue/B43Tiny.ml in your browser.
You should see:
open Tr1Bogue_Std.Bogue
let () =
if Tr1EntryName.module_id = __MODULE_ID__ then
Widget.label "Hello world"
|> Layout.resident
|> Bogue.of_layout
|> Bogue.run
Run it with:
./dk DkRun_V2_2.Run -- DkHelloScript_Std.B57Graphics.B43Bogue.B43Tiny
to see:
The Snoke game
So far you have seen a very simple use of the Bogue graphics library.
The author of Bogue has created a demonstration game which was “ported” to DkCoder.
The port did not change a single line of the original code. The directory structure was re-arranged (recall that there is a Java-like package mechanism underneath DkCoder) and an extra .ml
file was added.
Run it outside your project (perhaps your home directory) with:
git clone --branch V2_1 https://gitlab.com/diskuv/samples/dkcoder/SanetteBogue.git
./SanetteBogue/dk DkRun_V2_1.Run -- SanetteBogue_Snoke.Snoke
You should see a really fun game!
You can explore its GPL-3.0 licensed source code at https://gitlab.com/diskuv/samples/dkcoder/SanetteBogue.git
Making Your Own Script Project
Create an empty folder for your project. In the Terminal run:
git clone https://github.com/diskuv/dkcoder.git
dkcoder/dk user.dkcoder.project.init
✔️
DONE! You can edit the StartHere.ml
script, or add new scripts now.
Using scripts in Production
We’ve gone through the mechanics of creating your own projects and seeing what scripts can do on a desktop. However, scripts are not limited to running the desktop!
Let’s run a real production script:
git clone --branch V0_3 https://gitlab.com/diskuv/samples/devops/DkSubscribeWebhook.git
./DkSubscribeWebhook/dk DkRun_V0_3.Run -- DkSubscribeWebhook_Std.Subscriptions --help
You’ll be seeing the numerous help and options available to run the production webhook that manages the DkSDK subscriptions at DkSDK Pricing.
A webhook is a production microservice that responds to Internet requests from third parties. You could run the webhook yourself to manage your own customer subscriptions after you have configured some cloud SaaS services (more on this soon). The basic flow is:
- Stripe is the payments provider for DkSDK. Stripe sends an invoice.paid event to the webhook after establishing a subscription from the <https://diskuv.com/pricing> website.
- GitLab is the source control provider for DkSDK. A GitLab group token is created for the subscriber that expires after the subscription (plus a grace period).
- AWS SES is one of the email gateways used by DkSDK. An AWS SES email is sent to the subscriber containing the group token.
You could run the webhook script just like you did above. However, it is common to package up your scripts into a Docker container for deployment to production. Then in production when you execute:
docker-compose up --build
the 100MB webhook container image starts up with all the command line options and all the credentials to your cloud SaaS services. The Docker Compose example we provide also has a Let’s Encrypt web interface so that you can manage SSL certificates for the webhook.
I recommend you read the README at https://gitlab.com/diskuv/samples/devops/DkSubscribeWebhook.git if you would like to see the production scripts in detail.
What may not be obvious from looking at the whole DkSubscribeWebhook project is how it was developed. I encourage you to look at the git commit history to see how each SaaS provider was tested and developed separately as runnable scripts. Those same “provider” scripts can be used for manually administering subscriptions. They don’t bitrot and they compose well into the larger webhook service.
Parties Who Own Scripts
There are three parties of people who own DkCoder scripts:
- You (the first party)
- Us (the second party)
- Them (the third parties)
<p class="block">Here is a handy reference table that summarizes the distinctions. You won't understand the table right now and that is OK. We'll cover "Us" and "Them" in later sections in a future edition of DkCoder, but it is nice to have all the information in one place. All you need to remember is if you have a question about parties, this is the table to check.</p>
Party | Code Generators | Terms | Compiled Initially |
---|---|---|---|
Party | Code Generators | Terms | Compiled Initially |
You | DuneIde (default), Dune |
Checked | Yes |
Us | Dune |
No | |
Them | DuneIde (default), Dune |
Checked | No |
In this section we'll talk about where your You scripts go.
Your scripts must be placed in specific paths. Here is an example which you've seen in this walkthrough, and the general pattern you must follow:
.
└── src/
├── DkHelloScript_Std/
│ └── Example001.ml
└── <Libraryowner><Project>_<Unit>/
│── <Modulename1>.ml
└── <Modulename2>/
│── <Modulename3>.ml
│── <Modulename3>.mli
└── <Modulename4>/
└── <Modulename5>.ml
In the example above the DkHelloScript_Std
is a library. A library is an
organization of files and directories. These directories can have
subdirectories, and those subdirectories can have their own subdirectories
(etc.).
Any directory under src/
that is named in the format:
<Libraryowner><Project>_<Unit>
is also a library.
Component | Part | Why |
---|---|---|
Component | Part | Why |
Dk |
Library owner | A short identifier for which party owns the source code |
HelloScript |
Project | Conventionally this is a name related to your source code repository, but it can be any organizational name you want. You might, for example, want to name it your product name. |
Std |
Unit | Conventionally if you want to create a single library in a project, you use the name Std . Once you get too much code in your project, you can use the Unit to separate your code into more manageable, smaller pieces. |
We’ll define the exact format of <Libraryowner>
, <Project>
, and <Unit>
below.
You can place your script into the library directory, or into a subdirectory (or
subdirectory of a subdirectory, etc.) named according to the <Modulename>/
format defined below, as long as your script is named according to the
<Modulename>.ml
format. There is also an optional <Modulename>.mli
script
interface which is not covered in this article.
Part | Examples | Rules |
---|---|---|
Part | Examples | Rules |
Library owner | Dk , Acme , Blue123 |
The Library owner must start with an ASCII capital letter and have a lowercase ASCII second letter; the remaining characters (if any) must be lowercase ASCII letters and/or ASCII digits. |
Project | X , Ab , Widgets , WidgetsPlus |
The Project must start with an ASCII capital letter. The remaining characters (if any) must be ASCII letters (any case) and/or ASCII digits. Conventionally you use something similar to your source code repository name as the Project. |
Unit | Std , V1 , X |
The library unit must start with an ASCII capital letter. The remaining characters (if any) must be ASCII letters (any case) and/or ASCII digits and/or underscores (`_`). Conventially the main unit for your project is named `Std`. Once you get too much code in your project, you can use the Unit to separate your code into more manageable, smaller pieces. |
Examples | Counter Examples | Rule |
---|---|---|
Acme , Acme_Two , X |
DkHelloScript_Std |
The module name MUST NOT be a valid |
Acme , Acme*Two , X |
12345 , someThing | The module name must
start with an ASCII capital letter. The remaining characters (if any) must be
ASCII letters (any case) and/or ASCII digits and/or underscores
(* ). |
Standard OCaml Library - Stdlib
The Stdlib
standard library provides the basic operations over the built-in
types (numbers, booleans, byte sequences, strings, exceptions, references,
lists, arrays, input-output channels, …) and standard library modules.
In conventional OCaml programs the Stdlib
is automatically opened. That means
you can type print_endline "Hi"
rather than Stdlib.print_endline "Hi"
.
However, in DkCoder access to the Stdlib
is restricted: Stdlib
does a bit
too much. In particular, input-output channels and threading do not make sense
when compiling to JavaScript for use on a web page. Even more critical is that
the Stdlib.Obj
module is included, which has unsafe functions that make it
impossible to prove overall code safety.
You can explicitly get back the unsafe standard library by performing an open at the top of your scripts:
open Tr1Stdlib_V414
However, it is recommended to open the pieces of the standard library you actually need. That way if, for example, your script does not use files it can be compiled to JavaScript.
Package | Description | Modules |
---|---|---|
Package | Description | Modules |
Tr1Stdlib_V414CRuntime |
Modules that need a C99 runtime | Arg, Callback, Filename, Format, In_channel, LargeFile, Lexing, Out_channel, Printexc, Printf, Scanf, StdExit1, Sys, Unix, UnixLabels |
Tr1Stdlib_V414Io |
Modules for input and output. Can't be used on Android and iOS. | StdIo2 |
Tr1Stdlib_V414Threads |
Modules to create and coordinate threads. Brings in Tr1Stdlib_V414CRuntime . |
Condition, Event, Thread |
Tr1Stdlib_V414Gc |
Modules for garbage collection | Ephemeron, Gc, Weak |
Tr1Stdlib_V414Random |
Modules for random numbers | Random |
Tr1Stdlib_V414Unsafe |
Modules that can break type-safety | Dynlink, Marshal, Obj |
Tr1Stdlib_V414Base |
Modules safe for use on any platform. Also includes safe functions like sqrt . |
Array, ArrayLabels, Atomic, Bool, Buffer, Bytes, BytesLabels, Char, Complex, Digest, Either, Float, Fun, Hashtbl, Int, Int32, Int64, Lazy, List, ListLabels, Map, MoreLabels, Nativeint, Oo, Option, Parsing, Queue, Result, Seq, Set, Stack, StdBinds3, StdLabels, String, StringLabels, Uchar, Unit |
DkCoder Runtime Libraries
shexp.process
This is a library for creating possibly parallelized pipelines that represent shell scripts.
Docs: shexp README
ocaml-monadic
This small macro (PPX) library provides the let%bind
, if%bind
and
match%bind
forms. These are similar in design to Jane Street’s ppx_let
macros but work in 32-bit architectures and will be easier to support
backwards-compatibilty on OCaml 4.
This library also offers the ;%bind
, let%orzero
and [%guard]
forms.
Docs: ocaml-monadic README
Limitations imposed by cross-platform support.
DkSDK is a family of tools that supports cross-platform development, even on embedded devices. This cross-platform support places constraints on how you structure your DkCoder projects.
- The FAT32 filesystem is found on older Windows disks, CD drives and on
embedded devices. FAT32 only supports filenames up to 255 characters. DkCoder
limits the paths
<Libraryowner><Libraryproject>_<Libraryunit>/<Modulename>/.../<Modulename>.mli
to 240 characters. Even when the FAT32 filesystem entirely disappears from use this 240 character limit may remain since the limit encourages modular, single-focused libraries. - The FAT16 filesystem is the only filesystem in moderate use today that is not
supported by DkCoder. FAT16 might still be used on your older USB thumbdrives
and some very old embedded devices. Since FAT16 directory names are limited to
8 uppercase characters, and the minimum
<Libraryowner><Libraryproject>_<Libraryunit>
name is 7 characters, it is not reasonable to support FAT16.
Release Notes - Early Technical Limitations
-
On first install for Windows running the
./dk DkRun_V2_2.Run -- DkHelloScript_Std.Y33Article --serve
example can give:[00:00:22.564] [SUCCESS] (3/18) reproducibility or quick typing [00:00:22.564] Starting test: focus on what you run [00:00:22.566] [.\dk.cmd#3] '.\dk.cmd' DkRun_V2_1.Run '--generator=dune' -- DkHelloScript_Std.AndHelloAgain [ERROR][2024-04-29T00:00:43Z] /Run/ Failed to run C:\Users\WDAGUtilityAccount\DkHelloScript\src\DkHelloScript_Std\Y33Article.ml (DkHelloScript_Std.Y33Article). Code fa10b83d. Problem: The DkHelloScript_Std.Y33Article script exited with STATUS_ACCESS_VIOLATION (0xC0000005) - The instruction at 0x%08lx referenced memory at 0x%08lx. The memory could not be %s. Solution: Scroll up to see why.
If you rerun it, it succeeds.
-
The
./dk
script delegates to the build toolCMake
first, and then toOCaml
. That design reflects the history that DkCoder was designed first for C packages. The current design means:-
./dk
has the performance overhead of spawning CMake on startup. -
Ctrl-C does not cleanly kill all of the subprocesses on Windows. You may need to run
taskkill /f /im ocamlrunx.exe
to kill these hung processes. -
./dk
has command line arguments interpreted by CMake before being interpreted by your script. In particular, nested double-quotes like./dk ... "here is a nested `" double quote"
in PowerShell won't work.
-
-
The GUI code was very recently ported to Windows. It has not had memory auditing so segfaults may occur. And playing sounds is known to hang. Do not use for GUIs and music for production code until we message that this has been fixed.
Security
The primary security goal of DkCoder is to allow the script user to assert, before a script is run, whether the script does not access resources like files, the console, sound, etc. Not all scripts can be asserted, but if DkCoder asserts a script does not have access to files for exmaple, then the script does not have access to files under some light assumptions. These assumptions are detailed in this section.
# | ETA | What is Implemented |
---|---|---|
# | ETA | What is Implemented |
Alpha | Now | Split the OCaml Standard Library by resource type |
Beta | TBD | Implementation of the Technical Requirements below |
After GA | TBD | Allow users to write AWS Cedar policies to restrict access to a) resources used by running scripts and b) which scripts can be downloaded and c) data submitted to DkCoder services. |
Assumption A1 - A superset of modules used by a script can be obtained
This assumption is based on the OCaml 4.14 environment model Env.t
defined in
typing/env.mli.
These Env.t
have values, modules, types, module types, classes and class types
produced during compilation. Only add operations like add_value
are present,
so by the end of compilation we have a superset of statically compiled modules
used by the compiled script.
However,
-
Some of the modules may be functors; that is, new modules can be created at runtime of a specified module type. That leads to a technical requirement:
If a script uses a functor module, transitively or not, then the script is unassertable (aka. “tainted”).
How it will be implemented: The codept
analysis tool parses the source and
gathers module information, including whether a module is a functor. Using the
--log-level DEBUG
shows the modules used by the scripts.
Assumption A2 - Access to resources can be gated through modules
By definition, resources are either values like input channels or are accessed through values like function calls (including external C function calls).
In OCaml, values are present in a toplevel Env.t
or in a (possibly nested)
module and/or class.
-
Since there is no tool like
codept
to analyze OCaml classes, there is a technical requirement:If a script uses a class, transitively or not, then the script is unassertable (aka. “tainted”).
-
Since the standard OCaml toplevel environment is the contents of the
Stdlib
module (ie. there is an implicitopen Stdlib
at the top of each module), andStdlib
contains toplevel values likeprint_endline
, there is a technical requirement:All Stdlib toplevel values are annotated with OCaml alerts that fail the compilation when the toplevel values are used.
-
Since alerts can be locally overridden, there is a technical requirement:
If a script uses a local alert override, transitively or not, then the script is unassertable (aka. “tainted”).
To repeat: In OCaml, values are present in a toplevel Env.t
or in a (possibly
nested) module and/or class. What remains after implementing the technical
requirements above is that resource values necessary for safety assertions are
only present in (possibly nested) modules.
How it will be implemented: A PPX can be run that injects a new empty module
module Dk__Tainted = struct end
whenever a class
or local alert override is
used. The module analysis by codept
will discover the Dk__Tainted
, and the
analysis phase can fail.
Assumption A3 - Violating the correctness of OCaml type checking can be detected at compile time
The type-safe violations fall into these categories:
- Using an
external
C function declaration - Using the
Obj
module - Using the
Marshal
module
This is a weak assumption as it requires expert knowledge of OCaml.
Since the presence of modules can be trivially detected with assumption A1, the technical requirement is:
If a script uses an
external
C function declaration, transitively or not, then the script is unassertable (aka. “tainted”).
How it will be implemented: Any external
declaration can be caught by a PPX
which injects a module Dk__Tainted = struct end
. The presence of any of
Dk__Tainted
, Obj
or Marshal
during the codept
analysis will taint the
script.
Getting in touch