Plant Based UML Wiki

So you want to do some UML? Perhaps integrate some diagrams into a wiki, or even better, make the diagrams modifiable/update-able using said wiki? Well, that is what I wanted and since I prefer to work with nix, pandoc and gitit these became the weapons of choice.

systemd

Furthermore, I wanted a systemd user service load my personal wiki and I also didn’t want to use rely on configuration.nix , so I started from the following user service file:

0 ~ λ cat .config/systemd/user/gitit.service [ Unit ] Description= Personal Wiki [ Service ] Type= simple WorkingDirectory= %h/.gitit ExecStart= /home/edwtjo/.nix-profile/bin/gitit -f %h/.gitit/conf Restart= no [ Install ] WantedBy= console.target

That was easy enough, albeit a bit ugly that we hard code $HOME into the service file but such is the requirements of systemd. I also created a shell alias to work with user services:

alias userctl="systemctl --user"

This of course is not really important, just a neat alias I think is worth repeating.

GHC

Next up we need to make the plugins loadable by gitit, it wants to load .dyn_o files and there is some information regarding this here and here. Unfortunately I couldn’t figure out a way to change only the runtime default behaviour of GHC to compile dynamically using the haskell build expressions in nixpkgs. Also, using makewrapper and adding -dynamic-too to NIX_GHC turned out to be a fruitless exercise. At any rate; The manual way of compiling the plugins is to use, as noted in above links, -dynamic-too

cd ~/.gitit/plugins && ghc PlantUML.hs -dynamic-too

Gitit

Instead, I ended up inspecting the GHC API, and more specifically the GHC and DynFlags modules, with the intent of adding -dynamic-too to the plugin loader of Gitit. Perhaps unsurprisingly enough, there is a GeneralFlag constructor Opt_BuildDynamicToo which can be used with getSessionDynFlags and setSessionDynFlags in order to update the plugin loader mechanism of gitit. Also, it seems we’re in luck! There is a function gopt_set which takes a GeneralFlag and a DynFlags and flicks the switch in the generalFlags IntSet ! We now have a gitit diff:

diff --git a/src/Network/Gitit/Plugins.hs b/src/Network/Gitit/Plugins.hs index 0871d7a..ac85efe 100644 --- a/src/Network/Gitit/Plugins.hs +++ b/src/Network/Gitit/Plugins.hs @@ -29,6 +29,7 @@ import System.Log.Logger (logM, Priority(..)) #ifdef _PLUGINS import Data.List (isInfixOf, isPrefixOf) import GHC +import DynFlags (gopt_set, GeneralFlag(..)) import GHC.Paths import Unsafe.Coerce @@ -37,7 +38,7 @@ loadPlugin pluginName = do logM "gitit" WARNING ("Loading plugin '" ++ pluginName ++ "'...") runGhc (Just libdir) $ do dflags <- getSessionDynFlags - setSessionDynFlags dflags + setSessionDynFlags (gopt_set dflags Opt_BuildDynamicToo) defaultCleanupHandler dflags $ do -- initDynFlags unless ("Network.Gitit.Plugin." `isPrefixOf` pluginName)

And we save this to .nixpkgs/gitit-dyntoo.patch .

PlantUML

module PlantUML (plugin) where -- This plugin allows you to include a plantuml diagram -- in a page like this: -- -- ~~~ {.puml name="deployment"} -- @startuml -- cloud cloud1 -- cloud cloud2 -- cloud cloud3 -- cloud cloud4 -- cloud cloud5 -- cloud1 -0- cloud2 -- cloud1 -0)- cloud3 -- cloud1 -(0- cloud4 -- cloud1 -(0)- cloud5 -- @enduml -- ~~~ -- -- The "dot" and "plantuml" executable must be in the path. -- The generated png file will be saved in the static img directory. import GHC.IO.Handle import Network.Gitit.Interface import System.Process (readProcessWithExitCode) import System.Exit ( ExitCode ( ExitSuccess )) -- from the temporary package on HackageDB import System.IO.Temp (withTempFile) -- from the utf8-string package on HackageDB: import Data.ByteString.Lazy.UTF8 (fromString) import System.Environment (unsetEnv) import System.FilePath ((</>)) import System.FilePath plugin :: Plugin plugin = mkPageTransformM transformBlock transformBlock :: Block -> PluginM Block transformBlock ( CodeBlock (id, classes, namevals) contents) | "puml" `elem` classes = do cfg <- askConfig let filetype = "svg" outdir = staticDir cfg </> "img" liftIO $ withTempFile outdir "diag.puml" $ \infile inhandle -> do unsetEnv "DISPLAY" hPutStr inhandle contents hClose inhandle (ec, stdout, stderr) <- readProcessWithExitCode "plantuml" [ "-t" ++ filetype , infile ] "" let outname = takeFileName $ infile -<.> filetype if ec == ExitSuccess then return $ Para [ Image (id,classes,namevals) [ Str outname] ( "/img" </> outname, "" ) ] else error $ "plantuml returned an error status: " ++ stderr transformBlock x = return x

Simply, we use a temporary puml file in the static image directory, which gets deleted after the action finish. During the action we write the puml block contents to an input file and call out to plantuml, which in turn will write the output to the same place as the temporary file but with a new fileending. Also, we unset the DISPLAY environment variable since splash screens are just the worst. We could be smarter here and adapt to the target format but this will do for now.

We can now save this plugin to .gitit/plugins/PlantUML.hs and point to it in our gitit config like so:

... plugins: /home/edwtjo/.gitit/plugins/PlantUML.hs ...

Nix

So now all we have left is to create a gitit executable with plantuml added and all the dependencies of the plugin. We do this by creating a derivation in our .nixpkgs/config.nix :

... packageOverrides = self: with self: { wiki = zoom: let drv = let env = with haskell.lib ; haskellPackages.ghcWithPackages ( self : with self ; [ (appendPatches (doJailbreak gitit) [ ./gitit-dyntoo.patch ]) utf8-string temporary tagsoup ]); libDir = " ${env} /lib/ghc- ${env .version } " ; in stdenv.mkDerivation { name = "personal-wiki" ; phases = [ "installPhase" ] ; installPhase = let path = lib.makeBinPath [ gitFull plantuml graphviz texlive.combined.scheme-medium ] ; in '' makeWrapper \ " ${surf} /bin/surf -z ${toString zoom } 127.0.0.1:41934" \ $out /bin/wiki makeWrapper ${env} /bin/gitit $out /bin/gitit \ --prefix PATH : ${path} \ --set "NIX_GHC" " ${env} /bin/ghc" \ --set "NIX_GHC_LIBDIR" " ${libDir} " '' ; }; in lib.overrideDerivation drv (x : {buildInputs = x.buildInputs ++ [ makeWrapper ] ; } ) ; wiki_1 = wiki 1 ; wiki_2 = wiki 2 ; }; ...

You can ignore the zoom parameter, it is just something I use together with surf when I export presentations from gitit. Bear in mind that all packages are already in scope since we used self: with self; and keep the derivation in the same file. So we don’t need to actually add anything to buildInputs . In fact, we’re not really doing anything other than creating wrappers and we’re only using the installPhase from stdenv so we could’ve gotten away with a regular nix derivation. We do rely on the haskell library infrastructure to jailbreak gitit and apply the dyntoo patch from above.

We could, of course, also have added the gitit user service and gitit config as parametrized text files using Nix and have the wrapper create appropriate links in our home but this is left as an exercise to the reader.

Done

Nix install, start the user service, run the wiki command and create a new page containing:

~~~{.puml} @startuml Edward -> CoffeeMachine : switchOn activate CoffeeMachine CoffeeMachine -> Grinder : grindBeans deactivate CoffeeMachine activate Grinder Grinder -> Grinder : fetchBeans Grinder -> Grinder : runGrinder Grinder --> CoffeeMachine : BeansInFilterCode deactivate Grinder activate CoffeeMachine #DarkSalmon CoffeeMachine -> Boiler : heatWater deactivate CoffeeMachine activate Boiler Boiler -> Boiler : awaitTemperature Boiler --> CoffeeMachine : Temperature deactivate Boiler activate CoffeeMachine #red CoffeeMachine -> CoffeeMachine : runBrewer CoffeeMachine --> Edward : CoffeeCup deactivate CoffeeMachine @enduml ~~~

Which should render into:

Comments

Please enable JavaScript to view the comments powered by Disqus.