422 lines
12 KiB
Markdown
422 lines
12 KiB
Markdown
# 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)
|