# 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: 1. Run code from the project directory 2. Use `mix run` for one-shot execution 3. Use `iex -S mix` for sessions ## Steps ### Step 1: Add Mix configuration Add to `ob-elixir.el`: ```elisp ;;; 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 ```elisp ;;; 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 ```elisp ;;; 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`: ```elisp (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: ```elisp (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 ```elisp (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`: ```elisp ;;; 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`: ```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 path` executes in specified project - [ ] Auto-detection finds `mix.exs` in parent directories - [ ] `:mix-project no` disables auto-detection - [ ] `:mix-env` sets MIX_ENV correctly - [ ] Project modules are accessible - [ ] Sessions with `:mix-project` use `iex -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: ```bash 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 support - `test/test-ob-elixir-mix.el` - Add Mix tests ## References - [docs/04-elixir-integration-strategies.md](../docs/04-elixir-integration-strategies.md) - Mix Project Context - [Mix Documentation](https://hexdocs.pm/mix/Mix.html)