12 KiB
12 KiB
Task 08: Mix Project Support
Phase: 3 - Mix Integration Priority: High Estimated Time: 2-3 hours Dependencies: Task 07 (Session Support) or Phase 1 complete
Objective
Implement Mix project support so Elixir code can be executed within the context of a Mix project, with access to project dependencies and modules.
Prerequisites
- Phase 1 complete (or Phase 2 for session+mix)
- Understanding of Mix build tool
- A test Mix project
Background
Mix projects have:
- Dependencies in
mix.exs - Compiled modules in
_build/ - Configuration in
config/
To execute code in project context, we need:
- Run code from the project directory
- Use
mix runfor one-shot execution - Use
iex -S mixfor sessions
Steps
Step 1: Add Mix configuration
Add to ob-elixir.el:
;;; Mix Configuration
(defcustom ob-elixir-mix-command "mix"
"Command to run Mix."
:type 'string
:group 'ob-elixir
:safe #'stringp)
(defcustom ob-elixir-auto-detect-mix t
"Whether to automatically detect Mix projects.
When non-nil and no :mix-project is specified, ob-elixir will
search upward from the org file for a mix.exs file."
:type 'boolean
:group 'ob-elixir)
(defconst org-babel-header-args:elixir
'((mix-project . :any) ; Path to Mix project root
(mix-env . :any) ; MIX_ENV (dev, test, prod)
(mix-target . :any)) ; MIX_TARGET for Nerves, etc.
"Elixir-specific header arguments.")
Step 2: Implement Mix project detection
;;; Mix Project Detection
(defun ob-elixir--find-mix-project (&optional start-dir)
"Find Mix project root by searching for mix.exs.
Starts from START-DIR (default: current directory) and searches
upward. Returns the directory containing mix.exs, or nil."
(let* ((dir (or start-dir default-directory))
(found (locate-dominating-file dir "mix.exs")))
(when found
(file-name-directory found))))
(defun ob-elixir--resolve-mix-project (params)
"Resolve Mix project path from PARAMS or auto-detection.
Returns project path or nil."
(let ((explicit (cdr (assq :mix-project params))))
(cond
;; Explicit project path
((and explicit (not (eq explicit 'no)))
(expand-file-name explicit))
;; Explicitly disabled
((eq explicit 'no)
nil)
;; Auto-detect if enabled
(ob-elixir-auto-detect-mix
(ob-elixir--find-mix-project))
;; No project
(t nil))))
(defun ob-elixir--in-mix-project-p (params)
"Return t if execution should happen in Mix project context."
(not (null (ob-elixir--resolve-mix-project params))))
Step 3: Implement Mix execution
;;; Mix Execution
(defun ob-elixir--execute-with-mix (body result-type params)
"Execute BODY in Mix project context.
RESULT-TYPE is 'value or 'output.
PARAMS contains header arguments including :mix-project."
(let* ((project-dir (ob-elixir--resolve-mix-project params))
(mix-env (cdr (assq :mix-env params)))
(mix-target (cdr (assq :mix-target params)))
(default-directory project-dir)
(tmp-file (org-babel-temp-file "ob-elixir-mix-" ".exs"))
(code (if (eq result-type 'value)
(ob-elixir--wrap-for-value body)
body))
(env-vars (ob-elixir--build-mix-env mix-env mix-target)))
;; Write code to temp file
(with-temp-file tmp-file
(insert code))
;; Execute with mix run
(let ((command (format "%s%s run %s"
env-vars
ob-elixir-mix-command
(org-babel-process-file-name tmp-file))))
(ob-elixir--process-result
(shell-command-to-string command)))))
(defun ob-elixir--build-mix-env (mix-env mix-target)
"Build environment variable prefix for Mix execution."
(let ((vars '()))
(when mix-env
(push (format "MIX_ENV=%s" mix-env) vars))
(when mix-target
(push (format "MIX_TARGET=%s" mix-target) vars))
(if vars
(concat (mapconcat #'identity vars " ") " ")
"")))
Step 4: Update execute function
Modify org-babel-execute:elixir:
(defun org-babel-execute:elixir (body params)
"Execute a block of Elixir code with org-babel."
(let* ((session (cdr (assq :session params)))
(result-type (cdr (assq :result-type params)))
(result-params (cdr (assq :result-params params)))
(mix-project (ob-elixir--resolve-mix-project params))
(full-body (org-babel-expand-body:generic
body params
(org-babel-variable-assignments:elixir params)))
(result (cond
;; Session mode
((and session (not (string= session "none")))
(ob-elixir--evaluate-in-session
session full-body result-type params))
;; Mix project mode
(mix-project
(ob-elixir--execute-with-mix
full-body result-type params))
;; Plain execution
(t
(ob-elixir--execute full-body result-type)))))
(org-babel-reassemble-table
(org-babel-result-cond result-params
result
(ob-elixir--table-or-string result))
(org-babel-pick-name (cdr (assq :colname-names params))
(cdr (assq :colnames params)))
(org-babel-pick-name (cdr (assq :rowname-names params))
(cdr (assq :rownames params))))))
Step 5: Update session for Mix projects
Modify session creation to support Mix:
(defun ob-elixir--start-session (buffer-name session-name params)
"Start a new IEx session in BUFFER-NAME."
(let* ((mix-project (ob-elixir--resolve-mix-project params))
(mix-env (cdr (assq :mix-env params)))
(buffer (get-buffer-create buffer-name))
(default-directory (or mix-project default-directory))
(process-environment
(append
(list "TERM=dumb")
(when mix-env (list (format "MIX_ENV=%s" mix-env)))
process-environment)))
(with-current-buffer buffer
(if mix-project
;; Start with mix
(make-comint-in-buffer
(format "ob-elixir-%s" session-name)
buffer
ob-elixir-iex-command
nil
"-S" "mix")
;; Start plain IEx
(make-comint-in-buffer
(format "ob-elixir-%s" session-name)
buffer
ob-elixir-iex-command
nil))
;; Wait for prompt
(ob-elixir--wait-for-prompt buffer 30)
;; Configure IEx
(ob-elixir--configure-session buffer)
buffer)))
Step 6: Add compilation support
(defcustom ob-elixir-compile-before-run nil
"Whether to run mix compile before execution.
When non-nil, ensures project is compiled before running code.
This adds overhead but catches compilation errors early."
:type 'boolean
:group 'ob-elixir)
(defun ob-elixir--ensure-compiled (project-dir)
"Ensure Mix project at PROJECT-DIR is compiled."
(let ((default-directory project-dir))
(shell-command-to-string
(format "%s compile --force-check" ob-elixir-mix-command))))
(defun ob-elixir--execute-with-mix (body result-type params)
"Execute BODY in Mix project context."
(let* ((project-dir (ob-elixir--resolve-mix-project params))
(default-directory project-dir))
;; Optionally compile first
(when ob-elixir-compile-before-run
(ob-elixir--ensure-compiled project-dir))
;; ... rest of execution
))
Step 7: Add tests
Create test/test-ob-elixir-mix.el:
;;; test-ob-elixir-mix.el --- Mix project tests -*- lexical-binding: t; -*-
(require 'ert)
(require 'ob-elixir)
(defvar ob-elixir-test--mix-project-dir nil
"Temporary Mix project directory for testing.")
(defun ob-elixir-test--setup-mix-project ()
"Create a temporary Mix project for testing."
(let ((dir (make-temp-file "ob-elixir-test-" t)))
(setq ob-elixir-test--mix-project-dir dir)
(let ((default-directory dir))
;; Create mix.exs
(with-temp-file (expand-file-name "mix.exs" dir)
(insert "defmodule TestProject.MixProject do
use Mix.Project
def project do
[app: :test_project, version: \"0.1.0\", elixir: \"~> 1.14\"]
end
end"))
;; Create lib directory and module
(make-directory (expand-file-name "lib" dir))
(with-temp-file (expand-file-name "lib/test_project.ex" dir)
(insert "defmodule TestProject do
def hello, do: \"Hello from TestProject!\"
def add(a, b), do: a + b
end")))
dir))
(defun ob-elixir-test--cleanup-mix-project ()
"Clean up temporary Mix project."
(when ob-elixir-test--mix-project-dir
(delete-directory ob-elixir-test--mix-project-dir t)
(setq ob-elixir-test--mix-project-dir nil)))
;;; Tests
(ert-deftest ob-elixir-test-find-mix-project ()
"Test Mix project detection."
(skip-unless (executable-find ob-elixir-mix-command))
(unwind-protect
(let* ((project-dir (ob-elixir-test--setup-mix-project))
(sub-dir (expand-file-name "lib" project-dir))
(default-directory sub-dir))
(should (equal project-dir
(ob-elixir--find-mix-project))))
(ob-elixir-test--cleanup-mix-project)))
(ert-deftest ob-elixir-test-mix-project-execution ()
"Test code execution in Mix project context."
(skip-unless (and (executable-find ob-elixir-mix-command)
(executable-find ob-elixir-command)))
(unwind-protect
(let* ((project-dir (ob-elixir-test--setup-mix-project))
(params `((:mix-project . ,project-dir))))
;; Compile first
(let ((default-directory project-dir))
(shell-command-to-string "mix compile"))
;; Test execution
(let ((result (ob-elixir--execute-with-mix
"TestProject.hello()" 'value params)))
(should (string-match-p "Hello from TestProject" result))))
(ob-elixir-test--cleanup-mix-project)))
(ert-deftest ob-elixir-test-mix-env ()
"Test MIX_ENV handling."
(skip-unless (executable-find ob-elixir-mix-command))
(let ((env-str (ob-elixir--build-mix-env "test" nil)))
(should (string-match-p "MIX_ENV=test" env-str))))
(ert-deftest ob-elixir-test-explicit-no-mix ()
"Test disabling Mix with :mix-project no."
(let ((params '((:mix-project . no))))
(should (null (ob-elixir--resolve-mix-project params)))))
(provide 'test-ob-elixir-mix)
Step 8: Test in org buffer
Create Mix tests in test.org:
* Mix Project Tests
** Using project module (explicit path)
#+BEGIN_SRC elixir :mix-project ~/my_elixir_project
MyApp.hello()
#+END_SRC
** Using project module (auto-detect)
When this org file is inside a Mix project:
#+BEGIN_SRC elixir
MyApp.some_function()
#+END_SRC
** With specific MIX_ENV
#+BEGIN_SRC elixir :mix-project ~/my_elixir_project :mix-env test
Application.get_env(:my_app, :some_config)
#+END_SRC
** Session with Mix
#+BEGIN_SRC elixir :session mix-session :mix-project ~/my_elixir_project
# Has access to project modules
alias MyApp.SomeModule
#+END_SRC
** Disable auto-detect
#+BEGIN_SRC elixir :mix-project no
# Plain Elixir, no project context
1 + 1
#+END_SRC
Acceptance Criteria
:mix-project pathexecutes in specified project- Auto-detection finds
mix.exsin parent directories :mix-project nodisables auto-detection:mix-envsets MIX_ENV correctly- Project modules are accessible
- Sessions with
:mix-projectuseiex -S mix - Compilation errors are reported properly
- All tests pass
Header Arguments Reference
| Argument | Values | Description |
|---|---|---|
:mix-project |
path, no |
Project path or disable |
:mix-env |
dev, test, prod | MIX_ENV value |
:mix-target |
host, target | MIX_TARGET for Nerves |
Troubleshooting
Module not found
Ensure project is compiled:
cd /path/to/project && mix compile
Dependencies not available
Check that mix deps.get has been run.
Wrong MIX_ENV
Explicitly set :mix-env header argument.
Files Modified
ob-elixir.el- Add Mix supporttest/test-ob-elixir-mix.el- Add Mix tests
References
- docs/04-elixir-integration-strategies.md - Mix Project Context
- Mix Documentation