update
This commit is contained in:
@@ -1,40 +1,88 @@
|
|||||||
# Task 08: Mix Project Support
|
# Task 08: Mix Dependencies Support
|
||||||
|
|
||||||
**Phase**: 3 - Mix Integration
|
**Phase**: 3 - Mix Integration
|
||||||
**Priority**: High
|
**Priority**: High
|
||||||
**Estimated Time**: 2-3 hours
|
**Estimated Time**: 3-4 hours
|
||||||
**Dependencies**: Task 07 (Session Support) or Phase 1 complete
|
**Dependencies**: Task 07 (Session Support) or Phase 1 complete
|
||||||
|
|
||||||
## Objective
|
## 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.
|
Implement Mix dependencies support so users can specify dependencies directly in the org file using a special `#+BEGIN_DEPS` block. ob-elixir will automatically create a temporary Mix project with those dependencies, cache it for reuse, and use it as the execution context for subsequent Elixir code blocks.
|
||||||
|
|
||||||
## Prerequisites
|
## Prerequisites
|
||||||
|
|
||||||
- Phase 1 complete (or Phase 2 for session+mix)
|
- Phase 1 complete (or Phase 2 for session+deps)
|
||||||
- Understanding of Mix build tool
|
- Understanding of Mix build tool
|
||||||
- A test Mix project
|
- Network access for fetching dependencies
|
||||||
|
|
||||||
## Background
|
## Background
|
||||||
|
|
||||||
Mix projects have:
|
### The Problem
|
||||||
- Dependencies in `mix.exs`
|
|
||||||
- Compiled modules in `_build/`
|
|
||||||
- Configuration in `config/`
|
|
||||||
|
|
||||||
To execute code in project context, we need:
|
When writing Elixir code in org-mode, users often need access to external libraries (e.g., JSON parsing, HTTP clients, custom libraries). Without Mix project support, users are limited to Elixir's standard library.
|
||||||
1. Run code from the project directory
|
|
||||||
2. Use `mix run` for one-shot execution
|
### The Solution
|
||||||
3. Use `iex -S mix` for sessions
|
|
||||||
|
Instead of requiring users to point to an existing Mix project, ob-elixir will:
|
||||||
|
|
||||||
|
1. Parse dependency specifications from a special `#+BEGIN_DEPS` block
|
||||||
|
2. Create a temporary Mix project with those dependencies
|
||||||
|
3. Cache the project based on a hash of the dependencies (for fast re-evaluation)
|
||||||
|
4. Execute code blocks within that project context
|
||||||
|
5. Clean up temporary projects when Emacs exits
|
||||||
|
|
||||||
|
### Example Usage
|
||||||
|
|
||||||
|
```org
|
||||||
|
* Using External Dependencies
|
||||||
|
|
||||||
|
#+BEGIN_DEPS elixir
|
||||||
|
[
|
||||||
|
{:jason, "~> 1.4"},
|
||||||
|
{:prova, git: "git@gitlab.com:babel-upm/makina/prova.git"}
|
||||||
|
]
|
||||||
|
#+END_DEPS
|
||||||
|
|
||||||
|
#+BEGIN_SRC elixir
|
||||||
|
# Both jason and prova are available
|
||||||
|
Jason.encode!(%{hello: "world"})
|
||||||
|
#+END_SRC
|
||||||
|
|
||||||
|
#+BEGIN_SRC elixir :session deps-session
|
||||||
|
# Session also has access to deps
|
||||||
|
Prova.some_function()
|
||||||
|
#+END_SRC
|
||||||
|
|
||||||
|
* Different Dependencies Section
|
||||||
|
|
||||||
|
#+BEGIN_DEPS elixir
|
||||||
|
[{:httpoison, "~> 2.0"}]
|
||||||
|
#+END_DEPS
|
||||||
|
|
||||||
|
#+BEGIN_SRC elixir
|
||||||
|
# Now using httpoison instead of the previous deps
|
||||||
|
HTTPoison.get!("https://example.com")
|
||||||
|
#+END_SRC
|
||||||
|
```
|
||||||
|
|
||||||
|
## Design Decisions
|
||||||
|
|
||||||
|
| Aspect | Decision | Rationale |
|
||||||
|
|--------|----------|-----------|
|
||||||
|
| Block format | `#+BEGIN_DEPS elixir ... #+END_DEPS` | Special block, not confused with executable code |
|
||||||
|
| Deps syntax | Elixir list: `[{:pkg, "~> 1.0"}]` | Natural for Elixir users, supports all dep formats |
|
||||||
|
| Scope | All subsequent blocks until next deps block | Simple mental model |
|
||||||
|
| Caching | Hash-based | Fast re-evaluation, no redundant compilation |
|
||||||
|
| Multiple deps blocks | Later overrides | Allows different sections with different deps |
|
||||||
|
| Cleanup | Auto on Emacs exit | No orphaned temp projects |
|
||||||
|
| Elixir version | System default | Keep it simple, no version management |
|
||||||
|
|
||||||
## Steps
|
## Steps
|
||||||
|
|
||||||
### Step 1: Add Mix configuration
|
### Step 1: Add configuration and infrastructure
|
||||||
|
|
||||||
Add to `ob-elixir.el`:
|
|
||||||
|
|
||||||
```elisp
|
```elisp
|
||||||
;;; Mix Configuration
|
;;; Mix Dependencies Configuration
|
||||||
|
|
||||||
(defcustom ob-elixir-mix-command "mix"
|
(defcustom ob-elixir-mix-command "mix"
|
||||||
"Command to run Mix."
|
"Command to run Mix."
|
||||||
@@ -42,126 +90,241 @@ Add to `ob-elixir.el`:
|
|||||||
:group 'ob-elixir
|
:group 'ob-elixir
|
||||||
:safe #'stringp)
|
:safe #'stringp)
|
||||||
|
|
||||||
(defcustom ob-elixir-auto-detect-mix t
|
(defcustom ob-elixir-deps-cache-dir
|
||||||
"Whether to automatically detect Mix projects.
|
(expand-file-name "ob-elixir" (or (getenv "XDG_CACHE_HOME") "~/.cache"))
|
||||||
|
"Directory for caching temporary Mix projects.
|
||||||
|
|
||||||
When non-nil and no :mix-project is specified, ob-elixir will
|
Each unique set of dependencies gets its own subdirectory,
|
||||||
search upward from the org file for a mix.exs file."
|
named by the hash of the dependencies."
|
||||||
:type 'boolean
|
:type 'directory
|
||||||
:group 'ob-elixir)
|
:group 'ob-elixir)
|
||||||
|
|
||||||
(defconst org-babel-header-args:elixir
|
(defvar ob-elixir--deps-projects (make-hash-table :test 'equal)
|
||||||
'((mix-project . :any) ; Path to Mix project root
|
"Hash table mapping deps-hash to project directory path.")
|
||||||
(mix-env . :any) ; MIX_ENV (dev, test, prod)
|
|
||||||
(mix-target . :any)) ; MIX_TARGET for Nerves, etc.
|
(defvar ob-elixir--cleanup-registered nil
|
||||||
"Elixir-specific header arguments.")
|
"Whether the cleanup hook has been registered.")
|
||||||
```
|
```
|
||||||
|
|
||||||
### Step 2: Implement Mix project detection
|
### Step 2: Implement deps block parsing
|
||||||
|
|
||||||
```elisp
|
```elisp
|
||||||
;;; Mix Project Detection
|
;;; Deps Block Parsing
|
||||||
|
|
||||||
(defun ob-elixir--find-mix-project (&optional start-dir)
|
(defconst ob-elixir--deps-block-regexp
|
||||||
"Find Mix project root by searching for mix.exs.
|
"^[ \t]*#\\+BEGIN_DEPS[ \t]+elixir[ \t]*\n\\(\\(?:.*\n\\)*?\\)[ \t]*#\\+END_DEPS"
|
||||||
|
"Regexp matching a deps block.
|
||||||
|
Group 1 captures the deps content.")
|
||||||
|
|
||||||
Starts from START-DIR (default: current directory) and searches
|
(defun ob-elixir--find-deps-for-position (pos)
|
||||||
upward. Returns the directory containing mix.exs, or nil."
|
"Find the most recent deps block before POS.
|
||||||
(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)
|
Returns the deps content as a string, or nil if no deps block found."
|
||||||
"Resolve Mix project path from PARAMS or auto-detection.
|
(save-excursion
|
||||||
|
(goto-char pos)
|
||||||
|
(let ((found nil))
|
||||||
|
;; Search backward for deps blocks
|
||||||
|
(while (and (not found)
|
||||||
|
(re-search-backward ob-elixir--deps-block-regexp nil t))
|
||||||
|
(when (< (match-end 0) pos)
|
||||||
|
(setq found (match-string-no-properties 1))))
|
||||||
|
found)))
|
||||||
|
|
||||||
Returns project path or nil."
|
(defun ob-elixir--normalize-deps (deps-string)
|
||||||
(let ((explicit (cdr (assq :mix-project params))))
|
"Normalize DEPS-STRING for consistent hashing.
|
||||||
(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)
|
Removes comments, extra whitespace, and normalizes formatting."
|
||||||
"Return t if execution should happen in Mix project context."
|
(let ((normalized deps-string))
|
||||||
(not (null (ob-elixir--resolve-mix-project params))))
|
;; Remove Elixir comments
|
||||||
|
(setq normalized (replace-regexp-in-string "#.*$" "" normalized))
|
||||||
|
;; Normalize whitespace
|
||||||
|
(setq normalized (replace-regexp-in-string "[ \t\n]+" " " normalized))
|
||||||
|
;; Trim
|
||||||
|
(string-trim normalized)))
|
||||||
|
|
||||||
|
(defun ob-elixir--hash-deps (deps-string)
|
||||||
|
"Compute SHA256 hash of DEPS-STRING for caching."
|
||||||
|
(secure-hash 'sha256 (ob-elixir--normalize-deps deps-string)))
|
||||||
```
|
```
|
||||||
|
|
||||||
### Step 3: Implement Mix execution
|
### Step 3: Implement temp Mix project creation
|
||||||
|
|
||||||
```elisp
|
```elisp
|
||||||
;;; Mix Execution
|
;;; Temporary Mix Project Management
|
||||||
|
|
||||||
(defun ob-elixir--execute-with-mix (body result-type params)
|
(defconst ob-elixir--mix-exs-template
|
||||||
"Execute BODY in Mix project context.
|
"defmodule ObElixirTemp.MixProject do
|
||||||
|
use Mix.Project
|
||||||
|
|
||||||
RESULT-TYPE is 'value or 'output.
|
def project do
|
||||||
PARAMS contains header arguments including :mix-project."
|
[
|
||||||
(let* ((project-dir (ob-elixir--resolve-mix-project params))
|
app: :ob_elixir_temp,
|
||||||
(mix-env (cdr (assq :mix-env params)))
|
version: \"0.1.0\",
|
||||||
(mix-target (cdr (assq :mix-target params)))
|
elixir: \"~> 1.14\",
|
||||||
|
start_permanent: false,
|
||||||
|
deps: deps()
|
||||||
|
]
|
||||||
|
end
|
||||||
|
|
||||||
|
def application do
|
||||||
|
[extra_applications: [:logger]]
|
||||||
|
end
|
||||||
|
|
||||||
|
defp deps do
|
||||||
|
%s
|
||||||
|
end
|
||||||
|
end
|
||||||
|
"
|
||||||
|
"Template for temporary Mix project mix.exs file.
|
||||||
|
%s is replaced with the deps list.")
|
||||||
|
|
||||||
|
(defun ob-elixir--ensure-cache-dir ()
|
||||||
|
"Ensure the cache directory exists."
|
||||||
|
(unless (file-directory-p ob-elixir-deps-cache-dir)
|
||||||
|
(make-directory ob-elixir-deps-cache-dir t)))
|
||||||
|
|
||||||
|
(defun ob-elixir--get-deps-project (deps-string)
|
||||||
|
"Get or create a Mix project for DEPS-STRING.
|
||||||
|
|
||||||
|
Returns the project directory path, creating and initializing
|
||||||
|
the project if necessary."
|
||||||
|
(let* ((deps-hash (ob-elixir--hash-deps deps-string))
|
||||||
|
(cached (gethash deps-hash ob-elixir--deps-projects)))
|
||||||
|
(if (and cached (file-directory-p cached))
|
||||||
|
cached
|
||||||
|
;; Create new project
|
||||||
|
(ob-elixir--create-deps-project deps-hash deps-string))))
|
||||||
|
|
||||||
|
(defun ob-elixir--create-deps-project (deps-hash deps-string)
|
||||||
|
"Create a new temporary Mix project for DEPS-STRING.
|
||||||
|
|
||||||
|
DEPS-HASH is used as the directory name.
|
||||||
|
Returns the project directory path."
|
||||||
|
(ob-elixir--ensure-cache-dir)
|
||||||
|
(ob-elixir--register-cleanup)
|
||||||
|
|
||||||
|
(let* ((project-dir (expand-file-name deps-hash ob-elixir-deps-cache-dir))
|
||||||
|
(mix-exs-path (expand-file-name "mix.exs" project-dir))
|
||||||
|
(lib-dir (expand-file-name "lib" project-dir)))
|
||||||
|
|
||||||
|
;; Create project structure
|
||||||
|
(make-directory project-dir t)
|
||||||
|
(make-directory lib-dir t)
|
||||||
|
|
||||||
|
;; Write mix.exs
|
||||||
|
(with-temp-file mix-exs-path
|
||||||
|
(insert (format ob-elixir--mix-exs-template deps-string)))
|
||||||
|
|
||||||
|
;; Write a placeholder module
|
||||||
|
(with-temp-file (expand-file-name "ob_elixir_temp.ex" lib-dir)
|
||||||
|
(insert "defmodule ObElixirTemp do\nend\n"))
|
||||||
|
|
||||||
|
;; Fetch dependencies
|
||||||
|
(let ((default-directory project-dir))
|
||||||
|
(message "ob-elixir: Fetching dependencies...")
|
||||||
|
(let ((output (shell-command-to-string
|
||||||
|
(format "%s deps.get 2>&1" ob-elixir-mix-command))))
|
||||||
|
(when (string-match-p "\\*\\*.*error\\|Could not" output)
|
||||||
|
(error "ob-elixir: Failed to fetch dependencies:\n%s" output)))
|
||||||
|
|
||||||
|
;; Compile
|
||||||
|
(message "ob-elixir: Compiling dependencies...")
|
||||||
|
(let ((output (shell-command-to-string
|
||||||
|
(format "%s compile 2>&1" ob-elixir-mix-command))))
|
||||||
|
(when (string-match-p "\\*\\*.*[Ee]rror" output)
|
||||||
|
(error "ob-elixir: Failed to compile dependencies:\n%s" output))))
|
||||||
|
|
||||||
|
;; Cache and return
|
||||||
|
(puthash deps-hash project-dir ob-elixir--deps-projects)
|
||||||
|
(message "ob-elixir: Dependencies ready")
|
||||||
|
project-dir))
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 4: Implement execution with deps context
|
||||||
|
|
||||||
|
```elisp
|
||||||
|
;;; Execution with Dependencies
|
||||||
|
|
||||||
|
(defun ob-elixir--execute-with-deps (body result-type deps-string)
|
||||||
|
"Execute BODY with dependencies from DEPS-STRING.
|
||||||
|
|
||||||
|
RESULT-TYPE is 'value or 'output."
|
||||||
|
(let* ((project-dir (ob-elixir--get-deps-project deps-string))
|
||||||
(default-directory project-dir)
|
(default-directory project-dir)
|
||||||
(tmp-file (org-babel-temp-file "ob-elixir-mix-" ".exs"))
|
(tmp-file (org-babel-temp-file "ob-elixir-" ".exs"))
|
||||||
(code (if (eq result-type 'value)
|
(code (if (eq result-type 'value)
|
||||||
(ob-elixir--wrap-for-value body)
|
(ob-elixir--wrap-for-value body)
|
||||||
body))
|
body)))
|
||||||
(env-vars (ob-elixir--build-mix-env mix-env mix-target)))
|
|
||||||
|
|
||||||
;; Write code to temp file
|
;; Write code to temp file
|
||||||
(with-temp-file tmp-file
|
(with-temp-file tmp-file
|
||||||
(insert code))
|
(insert code))
|
||||||
|
|
||||||
;; Execute with mix run
|
;; Execute with mix run
|
||||||
(let ((command (format "%s%s run %s"
|
(let ((command (format "%s run --no-compile %s 2>&1"
|
||||||
env-vars
|
|
||||||
ob-elixir-mix-command
|
ob-elixir-mix-command
|
||||||
(org-babel-process-file-name tmp-file))))
|
(shell-quote-argument tmp-file))))
|
||||||
(ob-elixir--process-result
|
(ob-elixir--process-result
|
||||||
(shell-command-to-string command)))))
|
(shell-command-to-string command)))))
|
||||||
|
|
||||||
(defun ob-elixir--build-mix-env (mix-env mix-target)
|
(defun ob-elixir--start-session-with-deps (buffer-name session-name deps-string)
|
||||||
"Build environment variable prefix for Mix execution."
|
"Start a new IEx session with dependencies in BUFFER-NAME.
|
||||||
(let ((vars '()))
|
|
||||||
(when mix-env
|
SESSION-NAME is used for the process name.
|
||||||
(push (format "MIX_ENV=%s" mix-env) vars))
|
DEPS-STRING contains the dependencies specification."
|
||||||
(when mix-target
|
(let* ((project-dir (ob-elixir--get-deps-project deps-string))
|
||||||
(push (format "MIX_TARGET=%s" mix-target) vars))
|
(buffer (get-buffer-create buffer-name))
|
||||||
(if vars
|
(default-directory project-dir)
|
||||||
(concat (mapconcat #'identity vars " ") " ")
|
(process-environment (cons "TERM=dumb" process-environment)))
|
||||||
"")))
|
|
||||||
|
(with-current-buffer buffer
|
||||||
|
;; Start IEx with Mix project
|
||||||
|
(make-comint-in-buffer
|
||||||
|
(format "ob-elixir-%s" session-name)
|
||||||
|
buffer
|
||||||
|
ob-elixir-iex-command
|
||||||
|
nil
|
||||||
|
"-S" "mix")
|
||||||
|
|
||||||
|
;; Wait for initial prompt
|
||||||
|
(ob-elixir--wait-for-prompt buffer 30) ; Longer timeout for deps loading
|
||||||
|
|
||||||
|
;; Configure IEx for programmatic use
|
||||||
|
(ob-elixir--configure-session buffer)
|
||||||
|
|
||||||
|
buffer)))
|
||||||
```
|
```
|
||||||
|
|
||||||
### Step 4: Update execute function
|
### Step 5: Update main execute function
|
||||||
|
|
||||||
Modify `org-babel-execute:elixir`:
|
Modify `org-babel-execute:elixir` to check for deps context:
|
||||||
|
|
||||||
```elisp
|
```elisp
|
||||||
(defun org-babel-execute:elixir (body params)
|
(defun org-babel-execute:elixir (body params)
|
||||||
"Execute a block of Elixir code with org-babel."
|
"Execute a block of Elixir code with org-babel.
|
||||||
|
|
||||||
|
BODY is the Elixir code to execute.
|
||||||
|
PARAMS is an alist of header arguments."
|
||||||
(let* ((session (cdr (assq :session params)))
|
(let* ((session (cdr (assq :session params)))
|
||||||
(result-type (cdr (assq :result-type params)))
|
(result-type (cdr (assq :result-type params)))
|
||||||
(result-params (cdr (assq :result-params params)))
|
(result-params (cdr (assq :result-params params)))
|
||||||
(mix-project (ob-elixir--resolve-mix-project params))
|
;; Find deps for this block's position
|
||||||
|
(deps-string (ob-elixir--find-deps-for-position (point)))
|
||||||
|
;; Expand body with variable assignments
|
||||||
(full-body (org-babel-expand-body:generic
|
(full-body (org-babel-expand-body:generic
|
||||||
body params
|
body params
|
||||||
(org-babel-variable-assignments:elixir params)))
|
(org-babel-variable-assignments:elixir params)))
|
||||||
(result (cond
|
(result (cond
|
||||||
;; Session mode
|
;; Session mode with deps
|
||||||
|
((and session (not (string= session "none")) deps-string)
|
||||||
|
(ob-elixir--evaluate-in-session-with-deps
|
||||||
|
session full-body result-type deps-string))
|
||||||
|
;; Session mode without deps
|
||||||
((and session (not (string= session "none")))
|
((and session (not (string= session "none")))
|
||||||
(ob-elixir--evaluate-in-session
|
(ob-elixir--evaluate-in-session session full-body result-type))
|
||||||
session full-body result-type params))
|
;; Non-session with deps
|
||||||
;; Mix project mode
|
(deps-string
|
||||||
(mix-project
|
(ob-elixir--execute-with-deps full-body result-type deps-string))
|
||||||
(ob-elixir--execute-with-mix
|
|
||||||
full-body result-type params))
|
|
||||||
;; Plain execution
|
;; Plain execution
|
||||||
(t
|
(t
|
||||||
(ob-elixir--execute full-body result-type)))))
|
(ob-elixir--execute full-body result-type)))))
|
||||||
@@ -175,247 +338,397 @@ Modify `org-babel-execute:elixir`:
|
|||||||
(cdr (assq :rownames params))))))
|
(cdr (assq :rownames params))))))
|
||||||
```
|
```
|
||||||
|
|
||||||
### Step 5: Update session for Mix projects
|
### Step 6: Implement session support with deps
|
||||||
|
|
||||||
Modify session creation to support Mix:
|
|
||||||
|
|
||||||
```elisp
|
```elisp
|
||||||
(defun ob-elixir--start-session (buffer-name session-name params)
|
(defvar ob-elixir--session-deps (make-hash-table :test 'equal)
|
||||||
"Start a new IEx session in BUFFER-NAME."
|
"Hash table mapping session names to their deps-hash.
|
||||||
(let* ((mix-project (ob-elixir--resolve-mix-project params))
|
Used to ensure session deps consistency.")
|
||||||
(mix-env (cdr (assq :mix-env params)))
|
|
||||||
(buffer (get-buffer-create buffer-name))
|
(defun ob-elixir--get-or-create-session-with-deps (name deps-string params)
|
||||||
(default-directory (or mix-project default-directory))
|
"Get or create an IEx session NAME with DEPS-STRING."
|
||||||
(process-environment
|
(let* ((deps-hash (ob-elixir--hash-deps deps-string))
|
||||||
(append
|
(buffer-name (format "*ob-elixir:%s*" name))
|
||||||
(list "TERM=dumb")
|
(existing (get-buffer buffer-name))
|
||||||
(when mix-env (list (format "MIX_ENV=%s" mix-env)))
|
(existing-deps (gethash name ob-elixir--session-deps)))
|
||||||
process-environment)))
|
|
||||||
|
|
||||||
(with-current-buffer buffer
|
;; Check if existing session has different deps
|
||||||
(if mix-project
|
(when (and existing existing-deps (not (string= existing-deps deps-hash)))
|
||||||
;; Start with mix
|
(message "ob-elixir: Session %s has different deps, recreating..." name)
|
||||||
(make-comint-in-buffer
|
(ob-elixir-kill-session name)
|
||||||
(format "ob-elixir-%s" session-name)
|
(setq existing nil))
|
||||||
buffer
|
|
||||||
ob-elixir-iex-command
|
(if (and existing (org-babel-comint-buffer-livep existing))
|
||||||
nil
|
existing
|
||||||
"-S" "mix")
|
;; Create new session
|
||||||
;; Start plain IEx
|
(let ((buffer (ob-elixir--start-session-with-deps buffer-name name deps-string)))
|
||||||
(make-comint-in-buffer
|
(puthash name deps-hash ob-elixir--session-deps)
|
||||||
(format "ob-elixir-%s" session-name)
|
buffer))))
|
||||||
buffer
|
|
||||||
ob-elixir-iex-command
|
(defun ob-elixir--evaluate-in-session-with-deps (session body result-type deps-string)
|
||||||
nil))
|
"Evaluate BODY in SESSION with DEPS-STRING context."
|
||||||
|
(let* ((buffer (ob-elixir--get-or-create-session-with-deps session deps-string nil))
|
||||||
;; Wait for prompt
|
(code (if (eq result-type 'value)
|
||||||
(ob-elixir--wait-for-prompt buffer 30)
|
(ob-elixir--session-wrap-for-value body)
|
||||||
|
body))
|
||||||
;; Configure IEx
|
(eoe-indicator ob-elixir--eoe-marker)
|
||||||
(ob-elixir--configure-session buffer)
|
(full-body (concat code "\nIO.puts(\"" eoe-indicator "\")"))
|
||||||
|
output)
|
||||||
buffer)))
|
|
||||||
|
(unless buffer
|
||||||
|
(error "Failed to create Elixir session with deps: %s" session))
|
||||||
|
|
||||||
|
(setq output
|
||||||
|
(org-babel-comint-with-output
|
||||||
|
(buffer eoe-indicator t full-body)
|
||||||
|
(ob-elixir--send-command buffer full-body)))
|
||||||
|
|
||||||
|
(ob-elixir--clean-session-output output result-type)))
|
||||||
```
|
```
|
||||||
|
|
||||||
### Step 6: Add compilation support
|
### Step 7: Implement cleanup
|
||||||
|
|
||||||
```elisp
|
```elisp
|
||||||
(defcustom ob-elixir-compile-before-run nil
|
;;; Cleanup
|
||||||
"Whether to run mix compile before execution.
|
|
||||||
|
|
||||||
When non-nil, ensures project is compiled before running code.
|
(defun ob-elixir--register-cleanup ()
|
||||||
This adds overhead but catches compilation errors early."
|
"Register cleanup hook if not already registered."
|
||||||
:type 'boolean
|
(unless ob-elixir--cleanup-registered
|
||||||
:group 'ob-elixir)
|
(add-hook 'kill-emacs-hook #'ob-elixir-cleanup-deps-projects)
|
||||||
|
(setq ob-elixir--cleanup-registered t)))
|
||||||
|
|
||||||
(defun ob-elixir--ensure-compiled (project-dir)
|
(defun ob-elixir-cleanup-deps-projects ()
|
||||||
"Ensure Mix project at PROJECT-DIR is compiled."
|
"Delete all temporary Mix projects created by ob-elixir.
|
||||||
(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)
|
Called automatically on Emacs exit."
|
||||||
"Execute BODY in Mix project context."
|
(interactive)
|
||||||
(let* ((project-dir (ob-elixir--resolve-mix-project params))
|
(maphash
|
||||||
(default-directory project-dir))
|
(lambda (_hash project-dir)
|
||||||
|
(when (and project-dir (file-directory-p project-dir))
|
||||||
;; Optionally compile first
|
(condition-case err
|
||||||
(when ob-elixir-compile-before-run
|
(delete-directory project-dir t)
|
||||||
(ob-elixir--ensure-compiled project-dir))
|
(error
|
||||||
|
(message "ob-elixir: Failed to delete %s: %s" project-dir err)))))
|
||||||
;; ... rest of execution
|
ob-elixir--deps-projects)
|
||||||
))
|
(clrhash ob-elixir--deps-projects)
|
||||||
|
(message "ob-elixir: Cleaned up temporary Mix projects"))
|
||||||
|
|
||||||
|
(defun ob-elixir-list-deps-projects ()
|
||||||
|
"List all cached dependency projects."
|
||||||
|
(interactive)
|
||||||
|
(if (= (hash-table-count ob-elixir--deps-projects) 0)
|
||||||
|
(message "No cached dependency projects")
|
||||||
|
(with-output-to-temp-buffer "*ob-elixir deps projects*"
|
||||||
|
(princ "Cached ob-elixir dependency projects:\n\n")
|
||||||
|
(maphash
|
||||||
|
(lambda (hash dir)
|
||||||
|
(princ (format " %s\n -> %s\n" (substring hash 0 12) dir)))
|
||||||
|
ob-elixir--deps-projects))))
|
||||||
```
|
```
|
||||||
|
|
||||||
### Step 7: Add tests
|
### Step 8: Add tests
|
||||||
|
|
||||||
Create `test/test-ob-elixir-mix.el`:
|
Create `test/test-ob-elixir-deps.el`:
|
||||||
|
|
||||||
```elisp
|
```elisp
|
||||||
;;; test-ob-elixir-mix.el --- Mix project tests -*- lexical-binding: t; -*-
|
;;; test-ob-elixir-deps.el --- Deps block tests -*- lexical-binding: t; -*-
|
||||||
|
|
||||||
(require 'ert)
|
(require 'ert)
|
||||||
(require 'ob-elixir)
|
(require 'ob-elixir)
|
||||||
|
|
||||||
(defvar ob-elixir-test--mix-project-dir nil
|
;;; Parsing Tests
|
||||||
"Temporary Mix project directory for testing.")
|
|
||||||
|
|
||||||
(defun ob-elixir-test--setup-mix-project ()
|
(ert-deftest ob-elixir-test-deps-block-parsing ()
|
||||||
"Create a temporary Mix project for testing."
|
"Test finding deps blocks in buffer."
|
||||||
(let ((dir (make-temp-file "ob-elixir-test-" t)))
|
(with-temp-buffer
|
||||||
(setq ob-elixir-test--mix-project-dir dir)
|
(insert "#+BEGIN_DEPS elixir\n[{:jason, \"~> 1.4\"}]\n#+END_DEPS\n\n")
|
||||||
(let ((default-directory dir))
|
(insert "#+BEGIN_SRC elixir\nJason.encode!(%{})\n#+END_SRC\n")
|
||||||
;; Create mix.exs
|
(goto-char (point-max))
|
||||||
(with-temp-file (expand-file-name "mix.exs" dir)
|
(let ((deps (ob-elixir--find-deps-for-position (point))))
|
||||||
(insert "defmodule TestProject.MixProject do
|
(should deps)
|
||||||
use Mix.Project
|
(should (string-match-p ":jason" deps)))))
|
||||||
|
|
||||||
def project do
|
(ert-deftest ob-elixir-test-deps-block-override ()
|
||||||
[app: :test_project, version: \"0.1.0\", elixir: \"~> 1.14\"]
|
"Test that later deps blocks override earlier ones."
|
||||||
end
|
(with-temp-buffer
|
||||||
end"))
|
(insert "#+BEGIN_DEPS elixir\n[{:jason, \"~> 1.4\"}]\n#+END_DEPS\n\n")
|
||||||
;; Create lib directory and module
|
(insert "#+BEGIN_SRC elixir\ncode1\n#+END_SRC\n\n")
|
||||||
(make-directory (expand-file-name "lib" dir))
|
(insert "#+BEGIN_DEPS elixir\n[{:httpoison, \"~> 2.0\"}]\n#+END_DEPS\n\n")
|
||||||
(with-temp-file (expand-file-name "lib/test_project.ex" dir)
|
(let ((pos (point)))
|
||||||
(insert "defmodule TestProject do
|
(insert "#+BEGIN_SRC elixir\ncode2\n#+END_SRC\n")
|
||||||
def hello, do: \"Hello from TestProject!\"
|
(let ((deps (ob-elixir--find-deps-for-position (+ pos 10))))
|
||||||
def add(a, b), do: a + b
|
(should deps)
|
||||||
end")))
|
(should (string-match-p ":httpoison" deps))
|
||||||
dir))
|
(should-not (string-match-p ":jason" deps))))))
|
||||||
|
|
||||||
(defun ob-elixir-test--cleanup-mix-project ()
|
(ert-deftest ob-elixir-test-no-deps-block ()
|
||||||
"Clean up temporary Mix project."
|
"Test behavior when no deps block exists."
|
||||||
(when ob-elixir-test--mix-project-dir
|
(with-temp-buffer
|
||||||
(delete-directory ob-elixir-test--mix-project-dir t)
|
(insert "#+BEGIN_SRC elixir\n1 + 1\n#+END_SRC\n")
|
||||||
(setq ob-elixir-test--mix-project-dir nil)))
|
(should (null (ob-elixir--find-deps-for-position (point))))))
|
||||||
|
|
||||||
;;; Tests
|
(ert-deftest ob-elixir-test-deps-hash-consistency ()
|
||||||
|
"Test that same deps produce same hash."
|
||||||
|
(let ((deps1 "[{:jason, \"~> 1.4\"}]")
|
||||||
|
(deps2 "[{:jason, \"~> 1.4\"}]") ; extra space
|
||||||
|
(deps3 "[{:httpoison, \"~> 2.0\"}]"))
|
||||||
|
(should (string= (ob-elixir--hash-deps deps1)
|
||||||
|
(ob-elixir--hash-deps deps2)))
|
||||||
|
(should-not (string= (ob-elixir--hash-deps deps1)
|
||||||
|
(ob-elixir--hash-deps deps3)))))
|
||||||
|
|
||||||
(ert-deftest ob-elixir-test-find-mix-project ()
|
(ert-deftest ob-elixir-test-normalize-deps ()
|
||||||
"Test Mix project detection."
|
"Test deps normalization."
|
||||||
(skip-unless (executable-find ob-elixir-mix-command))
|
(should (string= (ob-elixir--normalize-deps "[ {:a, \"1\"} ]")
|
||||||
(unwind-protect
|
(ob-elixir--normalize-deps "[{:a,\"1\"}]")))
|
||||||
(let* ((project-dir (ob-elixir-test--setup-mix-project))
|
;; Comments are stripped
|
||||||
(sub-dir (expand-file-name "lib" project-dir))
|
(should (string= (ob-elixir--normalize-deps "[{:a, \"1\"}] # comment")
|
||||||
(default-directory sub-dir))
|
(ob-elixir--normalize-deps "[{:a, \"1\"}]"))))
|
||||||
(should (equal project-dir
|
|
||||||
(ob-elixir--find-mix-project))))
|
|
||||||
(ob-elixir-test--cleanup-mix-project)))
|
|
||||||
|
|
||||||
(ert-deftest ob-elixir-test-mix-project-execution ()
|
;;; Integration Tests (require network and mix)
|
||||||
"Test code execution in Mix project context."
|
|
||||||
|
(ert-deftest ob-elixir-test-deps-project-creation ()
|
||||||
|
"Test creating a temporary Mix project with deps."
|
||||||
(skip-unless (and (executable-find ob-elixir-mix-command)
|
(skip-unless (and (executable-find ob-elixir-mix-command)
|
||||||
(executable-find ob-elixir-command)))
|
(executable-find ob-elixir-command)))
|
||||||
(unwind-protect
|
(let ((ob-elixir-deps-cache-dir (make-temp-file "ob-elixir-test-" t))
|
||||||
(let* ((project-dir (ob-elixir-test--setup-mix-project))
|
(deps "[{:jason, \"~> 1.4\"}]"))
|
||||||
(params `((:mix-project . ,project-dir))))
|
(unwind-protect
|
||||||
;; Compile first
|
(let ((project-dir (ob-elixir--get-deps-project deps)))
|
||||||
(let ((default-directory project-dir))
|
(should (file-directory-p project-dir))
|
||||||
(shell-command-to-string "mix compile"))
|
(should (file-exists-p (expand-file-name "mix.exs" project-dir)))
|
||||||
;; Test execution
|
(should (file-directory-p (expand-file-name "deps/jason" project-dir))))
|
||||||
(let ((result (ob-elixir--execute-with-mix
|
;; Cleanup
|
||||||
"TestProject.hello()" 'value params)))
|
(ob-elixir-cleanup-deps-projects)
|
||||||
(should (string-match-p "Hello from TestProject" result))))
|
(delete-directory ob-elixir-deps-cache-dir t))))
|
||||||
(ob-elixir-test--cleanup-mix-project)))
|
|
||||||
|
|
||||||
(ert-deftest ob-elixir-test-mix-env ()
|
(ert-deftest ob-elixir-test-deps-execution ()
|
||||||
"Test MIX_ENV handling."
|
"Test executing code with deps."
|
||||||
|
(skip-unless (and (executable-find ob-elixir-mix-command)
|
||||||
|
(executable-find ob-elixir-command)))
|
||||||
|
(let ((ob-elixir-deps-cache-dir (make-temp-file "ob-elixir-test-" t))
|
||||||
|
(deps "[{:jason, \"~> 1.4\"}]"))
|
||||||
|
(unwind-protect
|
||||||
|
(let ((result (ob-elixir--execute-with-deps
|
||||||
|
"Jason.encode!(%{a: 1})"
|
||||||
|
'value
|
||||||
|
deps)))
|
||||||
|
(should (string-match-p "\"a\"" result)))
|
||||||
|
;; Cleanup
|
||||||
|
(ob-elixir-cleanup-deps-projects)
|
||||||
|
(delete-directory ob-elixir-deps-cache-dir t))))
|
||||||
|
|
||||||
|
(ert-deftest ob-elixir-test-deps-caching ()
|
||||||
|
"Test that deps projects are cached and reused."
|
||||||
(skip-unless (executable-find ob-elixir-mix-command))
|
(skip-unless (executable-find ob-elixir-mix-command))
|
||||||
(let ((env-str (ob-elixir--build-mix-env "test" nil)))
|
(let ((ob-elixir-deps-cache-dir (make-temp-file "ob-elixir-test-" t))
|
||||||
(should (string-match-p "MIX_ENV=test" env-str))))
|
(deps "[{:jason, \"~> 1.4\"}]"))
|
||||||
|
(unwind-protect
|
||||||
|
(let ((project1 (ob-elixir--get-deps-project deps))
|
||||||
|
(project2 (ob-elixir--get-deps-project deps)))
|
||||||
|
;; Should return same directory
|
||||||
|
(should (string= project1 project2))
|
||||||
|
;; Should only have one entry in hash table
|
||||||
|
(should (= 1 (hash-table-count ob-elixir--deps-projects))))
|
||||||
|
;; Cleanup
|
||||||
|
(ob-elixir-cleanup-deps-projects)
|
||||||
|
(delete-directory ob-elixir-deps-cache-dir t))))
|
||||||
|
|
||||||
(ert-deftest ob-elixir-test-explicit-no-mix ()
|
(provide 'test-ob-elixir-deps)
|
||||||
"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
|
### Step 9: Test in org buffer
|
||||||
|
|
||||||
Create Mix tests in `test.org`:
|
Create test cases in `test.org`:
|
||||||
|
|
||||||
```org
|
```org
|
||||||
* Mix Project Tests
|
* Deps Block Tests
|
||||||
|
|
||||||
** Using project module (explicit path)
|
** Basic deps usage
|
||||||
|
|
||||||
#+BEGIN_SRC elixir :mix-project ~/my_elixir_project
|
#+BEGIN_DEPS elixir
|
||||||
MyApp.hello()
|
[{:jason, "~> 1.4"}]
|
||||||
#+END_SRC
|
#+END_DEPS
|
||||||
|
|
||||||
** Using project module (auto-detect)
|
|
||||||
|
|
||||||
When this org file is inside a Mix project:
|
|
||||||
|
|
||||||
#+BEGIN_SRC elixir
|
#+BEGIN_SRC elixir
|
||||||
MyApp.some_function()
|
Jason.encode!(%{hello: "world", number: 42})
|
||||||
#+END_SRC
|
#+END_SRC
|
||||||
|
|
||||||
** With specific MIX_ENV
|
** Multiple dependencies
|
||||||
|
|
||||||
#+BEGIN_SRC elixir :mix-project ~/my_elixir_project :mix-env test
|
#+BEGIN_DEPS elixir
|
||||||
Application.get_env(:my_app, :some_config)
|
[
|
||||||
|
{:jason, "~> 1.4"},
|
||||||
|
{:decimal, "~> 2.0"}
|
||||||
|
]
|
||||||
|
#+END_DEPS
|
||||||
|
|
||||||
|
#+BEGIN_SRC elixir
|
||||||
|
num = Decimal.new("3.14159")
|
||||||
|
Jason.encode!(%{pi: Decimal.to_string(num)})
|
||||||
#+END_SRC
|
#+END_SRC
|
||||||
|
|
||||||
** Session with Mix
|
** Git dependencies
|
||||||
|
|
||||||
#+BEGIN_SRC elixir :session mix-session :mix-project ~/my_elixir_project
|
#+BEGIN_DEPS elixir
|
||||||
# Has access to project modules
|
[
|
||||||
alias MyApp.SomeModule
|
{:prova, git: "git@gitlab.com:babel-upm/makina/prova.git"}
|
||||||
|
]
|
||||||
|
#+END_DEPS
|
||||||
|
|
||||||
|
#+BEGIN_SRC elixir
|
||||||
|
# Use the prova library
|
||||||
|
Prova.hello()
|
||||||
#+END_SRC
|
#+END_SRC
|
||||||
|
|
||||||
** Disable auto-detect
|
** Session with deps
|
||||||
|
|
||||||
#+BEGIN_SRC elixir :mix-project no
|
#+BEGIN_DEPS elixir
|
||||||
# Plain Elixir, no project context
|
[{:jason, "~> 1.4"}]
|
||||||
1 + 1
|
#+END_DEPS
|
||||||
|
|
||||||
|
#+BEGIN_SRC elixir :session json-session
|
||||||
|
data = %{users: [%{name: "Alice"}, %{name: "Bob"}]}
|
||||||
|
#+END_SRC
|
||||||
|
|
||||||
|
#+BEGIN_SRC elixir :session json-session
|
||||||
|
Jason.encode!(data, pretty: true)
|
||||||
|
#+END_SRC
|
||||||
|
|
||||||
|
** Deps override
|
||||||
|
|
||||||
|
#+BEGIN_DEPS elixir
|
||||||
|
[{:httpoison, "~> 2.0"}]
|
||||||
|
#+END_DEPS
|
||||||
|
|
||||||
|
#+BEGIN_SRC elixir
|
||||||
|
# Jason no longer available here, httpoison is
|
||||||
|
{:ok, resp} = HTTPoison.get("https://httpbin.org/get")
|
||||||
|
resp.status_code
|
||||||
|
#+END_SRC
|
||||||
|
|
||||||
|
** No deps (plain execution)
|
||||||
|
|
||||||
|
#+BEGIN_SRC elixir
|
||||||
|
# This block has no deps context (before any deps block or in different file)
|
||||||
|
Enum.map(1..5, &(&1 * 2))
|
||||||
#+END_SRC
|
#+END_SRC
|
||||||
```
|
```
|
||||||
|
|
||||||
## Acceptance Criteria
|
## Acceptance Criteria
|
||||||
|
|
||||||
- [ ] `:mix-project path` executes in specified project
|
- [ ] `#+BEGIN_DEPS elixir` block is recognized and parsed
|
||||||
- [ ] Auto-detection finds `mix.exs` in parent directories
|
- [ ] Deps content is extracted correctly (Elixir list syntax)
|
||||||
- [ ] `:mix-project no` disables auto-detection
|
- [ ] Temp Mix project is created with correct `mix.exs`
|
||||||
- [ ] `:mix-env` sets MIX_ENV correctly
|
- [ ] `mix deps.get` runs automatically
|
||||||
- [ ] Project modules are accessible
|
- [ ] `mix compile` runs automatically
|
||||||
- [ ] Sessions with `:mix-project` use `iex -S mix`
|
- [ ] Subsequent elixir blocks can use the dependencies
|
||||||
- [ ] Compilation errors are reported properly
|
- [ ] Projects are cached by deps hash (identical deps = same project)
|
||||||
|
- [ ] Cache reuse is fast (no re-compilation)
|
||||||
|
- [ ] Sessions work with deps context (`iex -S mix`)
|
||||||
|
- [ ] Session maintains deps consistency (recreates if deps change)
|
||||||
|
- [ ] Later deps blocks override earlier ones for subsequent code blocks
|
||||||
|
- [ ] Auto-cleanup on Emacs exit works
|
||||||
|
- [ ] `ob-elixir-cleanup-deps-projects` command available
|
||||||
|
- [ ] `ob-elixir-list-deps-projects` command available
|
||||||
|
- [ ] Proper error handling for:
|
||||||
|
- Invalid dependency specs
|
||||||
|
- Network failures during `deps.get`
|
||||||
|
- Compilation errors
|
||||||
- [ ] All tests pass
|
- [ ] All tests pass
|
||||||
|
|
||||||
## Header Arguments Reference
|
## Header Arguments Reference
|
||||||
|
|
||||||
| Argument | Values | Description |
|
This feature does not add new header arguments. Instead, it uses special blocks:
|
||||||
|----------|--------|-------------|
|
|
||||||
| `:mix-project` | path, `no` | Project path or disable |
|
| Block | Format | Description |
|
||||||
| `:mix-env` | dev, test, prod | MIX_ENV value |
|
|-------|--------|-------------|
|
||||||
| `:mix-target` | host, target | MIX_TARGET for Nerves |
|
| `#+BEGIN_DEPS elixir` | Elixir list | Dependencies for subsequent code blocks |
|
||||||
|
|
||||||
|
Supported dependency formats (same as Mix):
|
||||||
|
|
||||||
|
```elixir
|
||||||
|
# Hex package
|
||||||
|
{:package_name, "~> 1.0"}
|
||||||
|
|
||||||
|
# Git repository
|
||||||
|
{:package_name, git: "https://github.com/user/repo.git"}
|
||||||
|
{:package_name, git: "git@github.com:user/repo.git", tag: "v1.0"}
|
||||||
|
{:package_name, git: "...", branch: "main"}
|
||||||
|
|
||||||
|
# Path (for local development)
|
||||||
|
{:package_name, path: "../local_package"}
|
||||||
|
|
||||||
|
# With options
|
||||||
|
{:package_name, "~> 1.0", only: :dev}
|
||||||
|
{:package_name, "~> 1.0", runtime: false}
|
||||||
|
```
|
||||||
|
|
||||||
## Troubleshooting
|
## Troubleshooting
|
||||||
|
|
||||||
### Module not found
|
### Dependencies not found
|
||||||
|
|
||||||
Ensure project is compiled:
|
Ensure the deps block is before the code block:
|
||||||
```bash
|
|
||||||
cd /path/to/project && mix compile
|
```org
|
||||||
|
#+BEGIN_DEPS elixir <-- Must come first
|
||||||
|
[{:jason, "~> 1.4"}]
|
||||||
|
#+END_DEPS
|
||||||
|
|
||||||
|
#+BEGIN_SRC elixir <-- Code block after deps
|
||||||
|
Jason.encode!(%{})
|
||||||
|
#+END_SRC
|
||||||
```
|
```
|
||||||
|
|
||||||
### Dependencies not available
|
### Network errors during deps.get
|
||||||
|
|
||||||
Check that `mix deps.get` has been run.
|
Check network connectivity. The error message will include Mix output.
|
||||||
|
|
||||||
### Wrong MIX_ENV
|
### Compilation errors
|
||||||
|
|
||||||
Explicitly set `:mix-env` header argument.
|
Check that dependency versions are compatible. Look at the error output for details.
|
||||||
|
|
||||||
|
### Slow first execution
|
||||||
|
|
||||||
|
The first execution with new deps will be slow (fetching + compiling). Subsequent executions with the same deps use the cached project.
|
||||||
|
|
||||||
|
### Cleanup
|
||||||
|
|
||||||
|
To manually clean up cached projects:
|
||||||
|
|
||||||
|
```
|
||||||
|
M-x ob-elixir-cleanup-deps-projects
|
||||||
|
```
|
||||||
|
|
||||||
|
To see what's cached:
|
||||||
|
|
||||||
|
```
|
||||||
|
M-x ob-elixir-list-deps-projects
|
||||||
|
```
|
||||||
|
|
||||||
## Files Modified
|
## Files Modified
|
||||||
|
|
||||||
- `ob-elixir.el` - Add Mix support
|
- `ob-elixir.el` - Add deps block support
|
||||||
- `test/test-ob-elixir-mix.el` - Add Mix tests
|
- `test/test-ob-elixir-deps.el` - Add deps tests
|
||||||
|
|
||||||
|
## Implementation Notes
|
||||||
|
|
||||||
|
### Why not use header arguments?
|
||||||
|
|
||||||
|
The deps list can be quite long and complex (multiple deps, git URLs, options). A dedicated block is more readable and maintainable than cramming it into a header argument.
|
||||||
|
|
||||||
|
### Why auto-cleanup?
|
||||||
|
|
||||||
|
Temporary projects can consume significant disk space (compiled deps). Auto-cleanup on exit prevents orphaned projects while still allowing cache reuse during a session.
|
||||||
|
|
||||||
|
### Why hash-based caching?
|
||||||
|
|
||||||
|
Computing a hash of the normalized deps string is fast and ensures that:
|
||||||
|
1. Identical deps always use the same cached project
|
||||||
|
2. Any change in deps (version, options, etc.) creates a new project
|
||||||
|
3. No false cache hits from similar-looking deps
|
||||||
|
|
||||||
## References
|
## References
|
||||||
|
|
||||||
- [docs/04-elixir-integration-strategies.md](../docs/04-elixir-integration-strategies.md) - Mix Project Context
|
|
||||||
- [Mix Documentation](https://hexdocs.pm/mix/Mix.html)
|
- [Mix Documentation](https://hexdocs.pm/mix/Mix.html)
|
||||||
|
- [Mix.Project deps](https://hexdocs.pm/mix/Mix.Tasks.Deps.html)
|
||||||
|
|||||||
Reference in New Issue
Block a user