Skip to content

emarifer/elixir_cli_todoapp

Repository files navigation

Elixir CLI Todoapp

Example CLI to-do list application written in Elixir with SQLite3 database persistence.




GitHub License Static Badge Static Badge


Features 🚀:

Elixir is an incredibly robust and elegant programming language. It excels above many other languages in the reliability of its solutions, fault tolerance (typical of languages running on the BEAM VM), distributed computing, and, above all, the ease and reliability with which concurrent systems are built.

Elixir is designed to do a lot of things really well but it’s hard to beat languages that were designed specifically to be good for CLI tools without trading off the things that make it great for all other stuff. Languages like Rust, Zig, and especially Go, due to their ease of use (remember that the powerful Docker CLI and the Docker system itself are written in Go), are more suitable for creating CLI tools. However, when building CLI tools with Elixir, you'll rarely need to use its powerful features and will instead have to deal with the occasional inconvenience.

Among other things, we can mention the boot times required by Elixir applications, given that the BEAM virtual machine must be started (between 200 and 500 ms, depending on the machine on which it is running, but less than the JVM 😅). If the application is used as a simple interactive tool, this isn't a problem, but if it's used as a looped command within a shell script, this boot time would be an unacceptable hindrance.

The other problem is the lower portability resulting from compilation artifacts. With the aforementioned languages, we get a single executable and, in the case of Go, very easy cross-compilation for various operating systems. With Elixir, we have several options, each with its pros and cons. With Elixir releases, we get a folder that includes not only the compiled code but also the Erlang virtual machine and its default execution environment, so they do not need to be installed on the host where the application is run. However, this solution works better with daemons than with foreground CLI applications. They can only run on the same OS and with the same version as the one on which they were compiled, and they are not a single executable but a folder. Mix Escript does provide a single, lighter executable but requires the host to have Erlang/OTP installed. This looks perfect but it has one downside: it doesn't support projects or dependencies that need to store or read from the priv directory, as is the case of the tzdata library, which we use in this project as a dependency of another library, Timex (it connects to the Internet at startup and downloads the time zone update from the IANA tz database).

Fortunately, to overcome most if not all limitations, and be able to produce a single binary artifact, there's a fantastic OSS library called Burrito (which we'll cover next). It lets you wrap your meaty app so you can delight your CLI app users!

There are many situations in which a CLI application written in Elixir will not suffer the aforementioned drawbacks, mainly in the case of interactive tools that perform tasks that can be tedious or useful for a user, as in the case of this application, which, in any case, is oriented towards learning this fantastic language that is Elixir 😀.

  • Using the Burrito library: As stated in Burrito's own documentation: "Burrito is our answer to the problem of distributing Elixir CLI applications across varied environments, where we cannot guarantee that the Erlang runtime is installed, and where we lack the permissions to install it ourselves". Burrito uses Mix releases so we get all their benefits as well as a self-extracting archive. It creates a native binary for macOS, Linux, and Windows. It is very configurable and allows cross-compilation with almost no restrictions 😀. Consequently, the resulting binary is distributable, and neither Elixir nor Erlang/OTP is required. When you run it for the first time, a folder containing the ERTS (Erlang Runtime System Application) and the BEAM virtual machine is automatically installed in the OS configuration folder.

  • Using the Timex library: We've already mentioned that this library uses tzdata under the hood, and we use it to make it easier to use time zones and format dates and times. Ecto (see below), by default, utilizes NaiveDateTime for timestamps in migrations and schemas. When using timestamps() in an Ecto schema, it generates two fields: inserted_at and updated_at. These fields default to using the naive_datetime type. If, on the other hand, we tell Ecto to store the timestamps with the utc_datetime type, this will save the UTC time in the database but will internally record the local time zone, which we can later "translate" into the time zone of the host running the application. All of this is probably unnecessary in an application like this, but it would make the database SQLite file more portable and, above all, allow us to learn how to use a new library 😅.

  • Using the Ecto library: Ecto (an official Elixir project) is a database wrapper and query language for the Elixir programming language. It provides a way to interact with SQL databases, manage data, and build queries using Elixir syntax. Ecto is not an ORM (Object-Relational Mapper) in the traditional sense, as it doesn't automatically track changes to data like some ORMs do. Instead, it focuses on providing a flexible and powerful way to interact with databases while giving developers more control over data management. With Ecto we’re able to create migrations, define schemas, insert and update records, and query them. Although Ecto recommends using PostgreSQL, for a CLI project it is more appropriate to use SQLite3

  • Finally, the application itself: is configured around a recursive loop that waits indefinitely for the user to enter a choice and responds accordingly. When the application starts (in the Application.start/2 callback), this function is called as a Task process within the application's supervisor tree. It ends when the user chooses the quit option, which calls System.halt/1. For this recursive loop we were inspired by these publications:

Getting Started 👨‍🚀:

  • Installing the application on our system (Window, Linux only):

    The easiest way is to download the executable for your system from here and run it from the terminal in the download directory.

    This action will automatically create a hidden folder (.burrito/todo_cli_app_erts-15.2.7_0.1.0/) in the user's applications configuration directory. This folder basically contains the Erlang virtual machine and the runtime, so the application contains everything necessary to start without having to install anything first. Likewise, another folder (todo_cli_app/) will be created in the user configuration folder containing the SQLite3 database storage file, and the migrations to create the table that will store the application data will be automatically executed. Once this is done, our application will start.

    Obviously, for user convenience, it's best to place the downloaded executable in any folder you want, but set its path in the system environment variable (PATH). Now, whenever we type the name of the executable (todo_cli_app) in the terminal, the application will launch.

    When we want to completely uninstall the application from our system, simply run the following command in the folder where the executable is located:

    $ ./my-binary maintenance uninstall
    

    This will delete the .burrito/todo_cli_app_erts-15.2.7_0.1.0/ folder. Next, we can delete the executable itself and the todo_cli_app/ folder in our configuration directory, where the database file is stored. Once all this is done, the application will be completely removed from the system.

  • Compiling the application ourselves:

    • Prerequisites:

      Obviously, you'll need to install Elixir and Erlang/OTP (because you'll need to use its virtual machine). I recommend doing this through asdf. This will allow you to have multiple versions of Elixir installed and easily switch between them per project or set a global version for the system.

      I advise you to follow the recommendations made here to create a more complete Erlang/OTP installation that will give you more options when developing with Elixir/Erlang.

      If you want to package the application into a single statically linked binary for distribution, you'll need to install Zig, since the burrito library creates a wrapper around the Elixir/Erlang application and packages it and all its dependencies into a single binary. It's important that the Zig version is 0.14.0. Doing so is very easy: just download the version, unzip the tar.xz or zip file corresponding to your system/architecture, and you can drop it anywhere on your system as long as you set the path to the resulting directory in the environment variable. Test to see if it works by running the following command in the terminal:

      $ zig version # ==> 0.14.0
      

      You will also need to have xz (on Linux/MacOS) or zip (on Windows), but this is usually already installed on your stock system.

      Now you are ready to start!

    • Build the binary:

      Clone the repository and go to its directory and in your text editor open the mix.exs file. In this file, modify the lines (commenting or uncommenting them) corresponding to the build targets you want.

      defp releases do
        [
          todo_cli_app: [
            steps: [:assemble, &Burrito.wrap/1],
            burrito: [
              targets: [
                linux: [os: :linux, cpu: :x86_64],     # <==
                # macos: [os: :darwin, cpu: :x86_64]   # <==
                windows: [os: :windows, cpu: :x86_64]  # <==
              ]
            ]
          ]
        ]
      end

      Cross-compiling from Linux/MacOS to any other target is well-tested and supported. However, cross-compiling from Windows may not always work. In any case, burrito is rapidly evolving, and these circumstances may change. For any questions or modifications to compilation options, I recommend that you carefully read the burrito library documentation.

      Now in the root directory of the project run the command you would run to create any Elixir release:

      $ MIX_ENV=prod mix release
      

      The build may take more or less time depending on your machine and the number of targets you have set. Finally, a folder (burrito_out/) will be created in the project root, containing the executables. If you go into this folder and call them from the terminal:

      $ todo_cli_app_linux # e.g.
      

      The application will be installed, creating a hidden folder (.burrito/todo_cli_app_erts-15.2.7_0.1.0/) in the user's applications configuration directory, which contains the Erlang virtual machine and runtime. A folder (todo_cli_app/) will also be created in the user configuration directory, containing the Sqlite3 database files. Next, the application will then start.

    • Developing/modifying the application in dev mode:

      If you have already cloned the repository you should follow the following steps.

      Download the necessary dependencies:

      $ mix deps.get
      

      Now all that remains is to start the application with the command:

      $ mix run --no-halt
      

      This command will create the Sqlite3 database files (in the project root itself, as defined in the dev.exs file of the application development configuration), and the connection to it, will automatically execute the migrations and start the application itself.

      Every time we make a change to the code, for it to be applied, we must stop the execution of the BEAM by typing Ctrl+C and then a and then call the previous command again.

  • Just for fun:

    Elixir and its built-in tool Mix offer the possibility of creating Tasks that, among other things, can help us automate actions that can always be tedious or error-prone.

    If you are a user of any Linux distribution you have the possibility of using 2 Mix tasks that I have created for this purpose. The first one (mix app.installer) serves to create the executable by using Elixir releases and the burrito library (as we did above) but also copies the executable to the user applications directory (your-user-folder/.local/bin/), renames it (eliminating the _linux suffix) and adds the path of the executable to the PATH environment variable (in the your-user-folder/.bashrc file) reloading its contents. The second one (mix app.uninstaller) undoes the previous actions, also deleting the folder containing the database files.

    In short, clone the repository and add the dependencies:

    $ mix deps.get
    

    If you want the compilation to take less time, comment out the line relative to the Windows target in the mix.exs file:

    defp releases do
      [
        todo_cli_app: [
          steps: [:assemble, &Burrito.wrap/1],
          burrito: [
            targets: [
              linux: [os: :linux, cpu: :x86_64]
              # macos: [os: :darwin, cpu: :x86_64]
              # windows: [os: :windows, cpu: :x86_64]  # <==
            ]
          ]
        ]
      ]
    end

    And now run the Mix task:

    $ mix app.installer
    

    To uninstall, run the following Mix task:

    $ mix app.uninstaller
    

    Obviously, none of these actions (both compilation and configuration/installation) are possible without the Elixir/Erlang suite installed on your computer. That's why these actions are usually executed using shell scripts or Makefiles, and I've only included the files corresponding to these Mix tasks as a practical exercise.


Happy coding 😀!!