Elixir

Diagnosing direnv issues using rtx

January 4, 2024 · 2 min read

I returned to a somewhat older Phoenix v1.6 application I had written that utilized direnv to load environment variables from a .env file. Coming from Laravel, I'm highly used to this workflow and I found that using something like direnv to inject the variables was better than the hacks I had been using at the time.

I started using asdf as a version-manager-of-all-trades before moving over to rtx. I liked its ergonomics and I had yet to run into an issue where I thought asdf was a better choice until now. The issue I was having was that for whatever reason direnv wasn't executing upon entering the directory as I had been used to.

Running direnv status showed output similar to the following:

direnv exec path /Users/jbrayton/.local/share/rtx/installs/direnv/2.32.2/bin/direnv
DIRENV_CONFIG /Users/jbrayton/.config/direnv
bash_path /usr/local/bin/bash
disable_stdin false
warn_timeout 5s
whitelist.prefix []
whitelist.exact map[]
No .envrc or .env loaded

There was more included but the key to focus on was No .envrc or .env loaded.

I ran through a couple of steps to try to figure out what was going on. I had found on their website that for the fish shell that I likely needed to wire up the direnv hook fish | source into my generic ~/.config/fish/config.fish file. To do that I installed direnv via homebrew because previously using it with rtx meant even my global usage wasn't global, or I was holding it wrong(tm).

What I was unaware of at the time was that when I went to update rtx I saw that homebrew changed the name to mise but the command mise wasn't found. After running brew install mise I was able to see the following migration output:

migrating /Users/jbrayton/.local/share/rtx/installs/elixir to /Users/jbrayton/.local/share/mise/installs
migrated rtx directories to mise
see https://mise.jdx.dev/rtx.html
migrating /Users/jbrayton/.config/rtx to /Users/jbrayton/.config/mise
migrated rtx directories to mise
see https://mise.jdx.dev/rtx.html

I'm making this post primarily for my own benefit though I seriously doubt I would ever run into this again on this machine or another. It's possible someone else may see similar weirdness with either one of the rtx plugins or something similar. From my understanding of all my other projects, rtx was working flawlessly except for this one instance but it turned out that direnv was broken for my entire system. The other projects that used it weren't working either.

If you see some weirdness with rtx and you haven't migrated, performing the migration may help you move forward like it did for me.

It's also worth noting that the migration doesn't copy your installs and I have 10GB of data in my old installs directory that I'll need to prune.

Livebook Tweaks

July 1, 2023 · 4 min read

I've learned a few things using Livebook that took a minute to uncover, so I wanted a place to keep it all together. I'll list them as a table of contents and explain further in each section, with links to my stumbling through issues and PRs.

  1. Version management: if you use a version manager like asdf or rtx, the Livebook version will be bound to your global or local instance, depending on the directory.
  2. MacOS app bundles Elixir: the Livebook desktop app is a great case study for bundling Erlang and Elixir.
  3. Reopening Livebook: if you close the Livebook window, you can open a new one by clicking on the Menu Bar icon and selecting Open.
  4. Menu Bar quirks: when running applications like Vanilla or Bartender, the icon only shows up if I restart Vanilla.
  5. Remote file management: the docker image defaults the working directory to /data, so if you use relative pathing in your notebooks or apps for storing files, you either have to infer this or change the WORKDIR in Docker.

Version Management

I spent maybe 20-30 minutes installing Livebook 0.9.3 and running it for my collection of notebooks. I would execute the command livebook server index.livemd in the directory, which ran version 1.14.4 at the time of discovery. When I upgraded Livebook, I was outside this directory which took in the global version 1.14.5. Because rtx or asdf completely isolates versions, I wasn't aware it was executing the 0.9.2 build with 1.14.4. Changing .tool-versions by using rtx use elixir@1.14.5-otp-25 brought my local environment up to global, and the Livebook version was what I expected.

I suspect this may bite me and others consistently if we're not careful. I'm sure I've had similar issues in the past that I did not equate to this at the time.

Desktop Elixir Bundler

A ton has changed since January 2022, but I opened this issue, which spurred a subsequent pull request. This work has slowly morphed into what is being called "ElixirKit" and is in the elixirkit/ directory in the repository. ElixirKit has a different surface area than Elixir Desktop, which is somewhat of a wrapper or orchestrator for wxWidgets in Elixir.

Reopening Livebook

In the past, when this was a Dock icon only, if you closed the browser window, it was less intuitive for me to open it again. I could reopen the window by clicking the Dock icon (see this issue for history). Now the app stays resident in the Menu Bar, and we can reopen windows or view logs to see the hidden console window. The view logs option is perfect for getting the auth token again. The dock also had issues fully closing the runtime but was ironed out <3.

I use the free version of the Vanilla app. If Livebook is on the always-visible right side, the icon will display whenever I close and reopen the desktop app. If it's on the default left side, I must quit Vanilla for the icon to appear. The long-term fix of keeping it visible is enough for me, but it may bite other people that use similar menu bar applications.

This issue spells out what I saw and may be helpful. I kept trying to run the app again, not knowing that it was there in the menu bar but I just couldn't see it.

Remote File Management

I had issues understanding how to work with notebooks or public apps on Huggingface. When I used something like data = "./data/file-sorted.csv" where the notebook-relative data/ directory existed, I would get problems like ** (File.Error) could not open "./data/file-sorted.csv": no such file or directory. My fix at the time was for apps to use something like data = System.get_env("PWD") to get the current working directory. This change posed a problem locally because my relative data/ directory is in .gitignore. Using PWD would save the data in the current directory, which would not fall under .gitignore rules.

The long-term fix, which you can see here, was to include WORKDIR "/notebooks" at the end of the file. WORKDIR tells Docker the working directory so that /data is now the place for only Livebook configuration, and any relative paths will work as expected.

There are other parts to this Dockerfile, such as pulling down my notebooks and the ash_tutorial so that I could work on Huggingface for things like Whisper or other ML models. Adding git or FFmpeg as dependencies is trivial. I explicitly copy the public-apps directory from my repository so that the Huggingface repo stays pure and only cares about setting up the environment.

One of my goals is to flesh out a system where I can work on notebooks remotely and periodically synchronize changes up to the repository. That's why I've included git-sync but I haven't worked out how to leverage it. A public or private app could leverage Kino.start_child/1 to start a GenServer that watches the filesystem for changes and presents a UI to commit and push changes. I believe something like egit could do this, I would need to create the UI for it. I'd certainly take this approach over shelling out to the git CLI though that's not extraordinarily difficult either.

Attending the DockYard Academy Beta Cohort

March 20, 2023 · 4 min read

I had the privilege of hanging around Brooklin Myers before he joined DockYard as an instructor in early 2022. A unique Elixir community slowly coalesced with the first video of the beta cohort starting on September 21st, 2022. I wouldn't join the group until October 15th as I wasn't sure what to make of it at first. I figured I would audit the class like I was some college kid.

The Curriculum

The academy skews toward junior developers or other Elixir newbies without previous formal instruction. Despite that, the curriculum and the commitment of 2 hours per day was an exceptional resource regardless of experience level.

The curriculum is not as lightweight as Elixir koans, and it is not as self-paced as Exercism's Elixir track. I hadn't been a part of the Exercism Elixir cohort on Discord, but I suspect it may have been similar.

What sets the curriculum apart is that it starts in Livebook, a low barrier to entry for learning Elixir. Eventually, it moves to bare mix new projects, graduating to full-on mix phx.new Phoenix applications. The beta curriculum experience was different than the first cohort, and there are upcoming changes for the second cohort. It's helpful to know the curriculum changes when pain points surface. There is no sleight of hand or abandonware as the official repository is what is taught from start to finish.

As someone that can have analysis paralysis at times when it comes to what and how to learn, having the path chosen for me was extremely helpful. Exercism gates the syllabus, but that can be daunting to decipher when you're starting. I also rushed through the concepts I was interested in rather than taking the time to enjoy the journey. I firmly believe the curriculum and Exercism complement each other very well.

The curriculum culminated in a capstone project, a chance to bundle all the skills we learned to produce our applications. The capstone sets it apart from other learning materials.

The Cohort

The beta cohort was a mix of Elixir newbies, seasoned Elixir developers and mentors, and people that hadn't touched a programming language. We experimented with teaching styles and nailed a cadence "locked in" at the last minute. Everyone I paired with showed remarkable improvement between October and the demo day on January 20th. That level of improvement is a testament to Brooklin's teaching style. Fundamentals became second nature very quickly. I would be lucky to work with anybody I met in the cohort or Discord server, as everyone grew into a developer. Elixir has a way of binding cohesive communities, but Brooklin truly has his superpower with the people around him. As much as I love DockYard, this felt like "The Brooklin Show" *sponsored by DockYard(tm)

Demo Day

I was one of the fewer resident developers to present on Demo Day, and that almost didn't happen. My capstone project, Beatseek, was hastily thrown together by duct tape. I had a working prototype at least a month before the deadline, but I had only given myself ten days from mix phx.new to what I presented. I thought it went well without a script, working through some prior presentations, but it was unpolished. I used sleight of hand as I do on some demos, but as a magician, I wanted to show all the tricks.

I didn't cut a public release until two months after demo day because I wasn't happy with what I produced. I had to retrofit tests, which exposed several shortcomings. If I had to do it again, I would choose anything other than id3 tags because the edge cases are absurdly complex.

What Would I Change?

I had a few issues working through the curriculum or with other cohort members. Tracking progress was difficult, but I used an Obsidian daily standup journal template to check off the table of contents manually. The standup journal became a good way of tracking changes over time, though there were few. The ramp-up to Phoenix for people with no web development or API exposure was pretty steep for the beta cohort, but I don't know if this is still true. Web development fundamentals span a breadth of knowledge, but the curriculum helps cement these concepts. People new to web development may wish to spend more time going through the same sections a few times until the concepts of things like MVC are less foreign. It'll make the later parts much easier to push through.

The End Result

I am 100% glad I had access to an instructor and mentor, even in a limited capacity. Everyone on the Discord server is excellent and a joy to be around. I would do this again in a heartbeat, but 2 hours was a sweet spot for someone like me with a full-time position to juggle. I can see how much more beneficial the 6-hour full day could be with more immersion, but that is a lot of material to cram. We had some luxury in drawing the material out and taking some time to keep everyone on the same pace.

Do you want to build an ElixirLS?

January 11, 2023 · 3 min read

There are certain instances where you may want to build ElixirLS to run against the version of Elixir/Erlang that you're using. The recommendation from the package is:

If you're packaging these archives in an IDE plugin, make sure to build using the minimum supported OTP version for the best backward-compatibility If you're like me, you may not care to support older versions of Elixir. How do we configure the plugin to run the latest version?

The output I see in VSCode's Output tab (Shift-Command-U on macOS) for the ElixirLS extension:

[Info  - 4:33:53 PM] Started ElixirLS v0.13.0
[Info  - 4:33:53 PM] ElixirLS built with elixir "1.12.3" on OTP "22"
[Info  - 4:33:53 PM] Running on elixir "1.14.2 (compiled with Erlang/OTP 25)" on OTP "25"
[Info  - 4:33:53 PM] Elixir sources not found (checking in /home/build/elixir). Code navigation to Elixir modules disabled.
[Info  - 4:33:54 PM] Loaded DETS databases in 32ms
[Info  - 4:33:54 PM] Starting build with MIX_ENV: test MIX_TARGET: host
[Info  - 4:33:55 PM] Compile took 854 milliseconds

There are numerous articles on building from source. What if we'd prefer to build the extension instead?

Let's unpack that Docker command to perform each step:

  1. Clone the repository with the latest branch: git clone --recursive --branch v0.13.0 https://github.com/elixir-lsp/vscode-elixir-ls.git /tmp/vscode-elixir-ls.
  2. Change to our temp directory: cd /tmp/vscode-elixir-ls.
  3. Install npm dependencies: npm install.
  4. Change to the elixir-ls directory: cd elixir-ls.
  5. Install Elixir dependencies: mix deps.get.
  6. Change to the parent directory: cd ...
  7. Package the extension: npx vsce package.
  8. Make an extensions directory in $HOME: mkdir -p $HOME/extensions.
  9. Copy the extension: cp /tmp/vscode-elixir-ls/elixir-ls-0.13.0.vsix $HOME/extensions.
  10. Remove the temporary directory: rm -rf /tmp/vscode-elixir-ls.

It is crucial to install Elixir v1.14.x and Erlang 25.1.x using your favorite method prior to packaging the new extension. I'm using asdf global to do this, but you could create a local .tool-versions inside the tmp folder if you wish. The extension should now live at /tmp/vscode-elixir-ls/elixir-ls-0.13.0.vsix.

The remaining steps copy the package to a directory the Docker container knows, and it's okay to stop here. Because the prepublish.bash file that executes at step #7 runs mix deps.get, we can eliminate steps 4, 5, and 6. These commands also compile the extension using MIX_ENV=dev, which we may not want. To change this, we can edit the last line in prepublish.bash to MIX_ENV=prod mix elixir_ls.release -o ../elixir-ls-release to compile for production.

Putting all of the (now reduced) commands together:

git clone --recursive --branch v0.13.0 https://github.com/elixir-lsp/vscode-elixir-ls.git /tmp/vscode-elixir-ls
cd /tmp/vscode-elixir-ls
npm install
npx vsce package
mkdir -p $HOME/extensions
cp /tmp/vscode-elixir-ls/elixir-ls-0.13.0.vsix $HOME/extensions
rm -rf /tmp/vscode-elixir-ls

We can install the extension from the VSIX file using the UI or the command code --install-extension $HOME/extensions/elixir-ls-0.13.0.vsix. To take advantage of the new extension in our projects, we need to rm -rf .elixir_ls and navigate to an Elixir file. ElixirLS won't start compiling until an Elixir file is open in the editor, and it'll usually take a few minutes to rebuild everything.

With the new extension installed we should see the change in VSCode's Output tab:

[Info  - 4:35:42 PM] Started ElixirLS v0.13.0
[Info  - 4:35:43 PM] ElixirLS built with elixir "1.14.2" on OTP "25"
[Info  - 4:35:43 PM] Running on elixir "1.14.2 (compiled with Erlang/OTP 25)" on OTP "25"
[Info  - 4:35:43 PM] Elixir sources not found (checking in /home/build/elixir). Code navigation to Elixir modules disabled.
[Info  - 4:35:48 PM] Loaded DETS databases in 414ms
[Info  - 4:35:48 PM] Starting build with MIX_ENV: test MIX_TARGET: host
[Info  - 4:35:49 PM] Compile took 1811 milliseconds

asdf Seems Broken After Homebrew Upgrade

January 9, 2023 · 1 min read

I upgraded the excellent asdf version manager using Homebrew and ran into a snag when trying to perform mix commands. I encounterd the error /Users/jbrayton/.asdf/shims/mix: line 13: /usr/local/Cellar/asdf/0.10.2/libexec/bin/asdf: No such file or directory. The key to notice here is the path /usr/local/Cellar/asdf/0.10.2/ when the newest version is 0.11.0, as there is clearly a mismatch. I restarted my terminal and shell, but the problem persisted. I noticed all the files in ~/.asdf/shims had the line exec /usr/local/Cellar/asdf/0.10.2/libexec/bin/asdf exec "odbcserver" "$@" # asdf_allow: ' asdf '. This line is not what we wanted and indicates the problem.

After looking at the pinned https://github.com/asdf-vm/asdf/issues/785 and then following that to https://github.com/asdf-vm/asdf/issues/1393, the solution rm -rf ~/.asdf/shims; asdf reshim fixes my problem. Now, whenever I examine one of the shim files, I see the line exec /usr/local/opt/asdf/libexec/bin/asdf exec "mix" "$@" # asdf_allow: ' asdf ' as expected. The directory /usr/local/opt is what I see when I run the command brew --prefix asdf as the prefix is no longer /usr/local/Cellar/asdf/0.10.2/ or the Cellar location. This corrective measure should be a more permanent solution moving forward as the prefix /usr/local/opt should no longer change in the future.

This issue was also somewhat of a perfect storm as Phoenix 1.7 rc.1 dropped two days ago and I had just upgraded a bunch of homebrew packages, including asdf.