diff --git a/lib/atomvm_builder.ex b/lib/atomvm_builder.ex new file mode 100644 index 0000000..392f1cf --- /dev/null +++ b/lib/atomvm_builder.ex @@ -0,0 +1,315 @@ +defmodule ExAtomVM.AtomVMBuilder do + @moduledoc """ + Shared utilities for building AtomVM from source. + + Provides common functionality used by platform-specific build tasks (ESP32, STM32, Pico, etc.): + + * Git repository management (clone, update, checkout) + * Generic Unix build (PackBEAM, atomvmlib, exavmlib, esp32boot) + * AVM library copying to `avm_deps/` + + Platform-specific build tasks should delegate to this module for shared operations. + """ + + @default_atomvm_url "https://github.com/atomvm/AtomVM" + + @doc """ + Clone or update an AtomVM repository at the given URL and ref. + + Returns the path to the local repository. + """ + def clone_or_update_repo(url \\ @default_atomvm_url, ref) do + cache_dir = + if Code.ensure_loaded?(Mix.Project) do + Path.join(Path.dirname(Mix.Project.build_path()), "atomvm_source") + else + Path.expand("_build/atomvm_source") + end + + repo_name = url |> Path.basename() |> String.replace(".git", "") + repo_path = Path.join(cache_dir, repo_name) + + if File.dir?(Path.join(repo_path, ".git")) do + update_repo(repo_path, ref) + else + clone_repo(url, repo_path, cache_dir, ref) + end + end + + @doc """ + Build generic Unix tools and libraries from an AtomVM source tree. + + Builds PackBEAM, elixir_esp32boot, exavmlib, and atomvmlib using cmake + ninja/make. + + ## Options + + * `atomvm_path` - Path to AtomVM source repository + * `mbedtls_prefix` - Optional path to custom MbedTLS installation + * `clean` - Whether to clean the build directory first + """ + def build_generic_unix(atomvm_path, mbedtls_prefix \\ nil, clean \\ false) do + build_dir = Path.join(atomvm_path, "build") + + if clean and File.dir?(build_dir) do + IO.puts("Cleaning generic Unix build directory...") + File.rm_rf!(build_dir) + end + + packbeam_path = Path.join([build_dir, "tools", "packbeam", "PackBEAM"]) + esp32boot_path = Path.join([build_dir, "libs", "esp32boot", "elixir_esp32boot.avm"]) + + if File.exists?(packbeam_path) and File.exists?(esp32boot_path) do + IO.puts("Generic Unix build tools and elixir_esp32boot already exist, skipping...") + :ok + else + IO.puts("Building generic Unix tools and elixir_esp32boot (required for build)...") + File.mkdir_p!(build_dir) + + {build_tool, cmake_generator} = + case System.find_executable("ninja") do + nil -> + IO.puts("Ninja not found, using Make as build system") + {"make", []} + + _ninja_path -> + IO.puts("Using Ninja as build system") + {"ninja", ["-GNinja"]} + end + + mbedtls_args = + if mbedtls_prefix do + IO.puts("Using custom MbedTLS from: #{mbedtls_prefix}") + ["-DCMAKE_PREFIX_PATH=#{mbedtls_prefix}"] + else + [] + end + + cmake_args = + [".."] ++ + mbedtls_args ++ + cmake_generator ++ ["-DCMAKE_BUILD_TYPE=Release", "-DAVM_BUILD_RUNTIME_ONLY=ON"] + + {_output, status} = + System.cmd("cmake", cmake_args, + cd: build_dir, + stderr_to_stdout: true, + into: IO.stream(:stdio, :line) + ) + + case status do + 0 -> + IO.puts("Building tools and elixir_esp32boot...") + + {_output, status} = + System.cmd(build_tool, ["PackBEAM", "elixir_esp32boot", "exavmlib", "atomvmlib"], + cd: build_dir, + stderr_to_stdout: true, + into: IO.stream(:stdio, :line) + ) + + case status do + 0 -> + IO.puts("Generic Unix tools and elixir_esp32boot built successfully") + :ok + + _ -> + {:error, "Failed to build generic Unix tools"} + end + + _ -> + {:error, "Failed to configure generic Unix build"} + end + end + end + + @doc """ + Copy built AVM libraries from the AtomVM build tree into the project's `avm_deps/` directory. + + This is needed for platforms (like STM32, Pico) where the AVM libraries must be bundled + into the application `.avm` file. ESP32 does NOT need this — its libraries are baked into + the firmware image and flashed to a separate partition. + """ + def copy_avm_libraries(atomvm_path) do + avm_deps_dir = File.cwd!() |> Path.join("avm_deps") + + if File.dir?(avm_deps_dir) do + IO.puts("Removing existing avm_deps folder...") + File.rm_rf!(avm_deps_dir) + end + + IO.puts("Creating avm_deps folder and copying libraries...") + File.mkdir_p!(avm_deps_dir) + + build_libs_dir = atomvm_path |> Path.join("build") |> Path.join("libs") + avm_files = build_libs_dir |> Path.join("**/*.avm") |> Path.wildcard() + + case avm_files do + [] -> + IO.puts("Warning: No .avm files found in #{build_libs_dir}") + :ok + + files -> + Enum.each(files, fn src_path -> + dest_path = src_path |> Path.basename() |> then(&Path.join(avm_deps_dir, &1)) + File.cp!(src_path, dest_path) + IO.puts(" Copied #{Path.basename(src_path)}") + end) + + IO.puts("✓ Copied #{length(files)} AVM libraries to #{avm_deps_dir}") + :ok + end + end + + # --- Private git helpers --- + + defp clone_repo(url, repo_path, cache_dir, ref) do + IO.puts("Cloning #{url}") + File.mkdir_p!(cache_dir) + + {output, status} = + System.cmd("git", ["clone", url, repo_path], stderr_to_stdout: true) + + case status do + 0 -> + IO.puts(output) + checkout_ref(repo_path, ref) + + _ -> + IO.puts("Error cloning repository:\n#{output}") + exit({:shutdown, 1}) + end + end + + defp update_repo(repo_path, ref) do + IO.puts("Updating existing repository at #{repo_path}") + + {output, status} = + System.cmd("git", ["fetch", "origin"], + cd: repo_path, + stderr_to_stdout: true + ) + + case status do + 0 -> + IO.puts(output) + checkout_ref(repo_path, ref) + + _ -> + IO.puts("Error fetching from repository:\n#{output}") + exit({:shutdown, 1}) + end + end + + defp checkout_ref(repo_path, ref) do + System.cmd("git", ["reset", "--hard"], + cd: repo_path, + stderr_to_stdout: true + ) + + case parse_pr_ref(ref) do + {:pr, pr_number} -> + fetch_and_checkout_pr(repo_path, pr_number) + + :not_pr -> + IO.puts("Checking out ref: #{ref}") + + {output, status} = + System.cmd("git", ["checkout", ref], + cd: repo_path, + stderr_to_stdout: true + ) + + case status do + 0 -> + IO.puts(output) + pull_if_branch(repo_path, ref) + + _ -> + IO.puts("Error checking out ref:\n#{output}") + exit({:shutdown, 1}) + end + end + end + + defp parse_pr_ref(ref) do + cond do + match = Regex.run(~r/^pull\/(\d+)\/head$/, ref) -> + {:pr, Enum.at(match, 1)} + + match = Regex.run(~r/^pr\/(\d+)$/, ref) -> + {:pr, Enum.at(match, 1)} + + true -> + :not_pr + end + end + + defp fetch_and_checkout_pr(repo_path, pr_number) do + branch = "pr-#{pr_number}" + + IO.puts("Fetching PR ##{pr_number}...") + + {output, status} = + System.cmd("git", ["fetch", "origin", "pull/#{pr_number}/head"], + cd: repo_path, + stderr_to_stdout: true + ) + + case status do + 0 -> + IO.puts(output) + + {output, status} = + System.cmd("git", ["checkout", "-B", branch, "FETCH_HEAD"], + cd: repo_path, + stderr_to_stdout: true + ) + + case status do + 0 -> + IO.puts(output) + IO.puts("Checked out PR ##{pr_number}") + repo_path + + _ -> + IO.puts("Error checking out PR branch:\n#{output}") + exit({:shutdown, 1}) + end + + _ -> + IO.puts("Error fetching PR ##{pr_number}:\n#{output}") + exit({:shutdown, 1}) + end + end + + defp pull_if_branch(repo_path, ref) do + {_output, status} = + System.cmd("git", ["symbolic-ref", "-q", "HEAD"], + cd: repo_path, + stderr_to_stdout: true + ) + + if status == 0 do + IO.puts("Pulling latest changes for branch #{ref}") + + {output, status} = + System.cmd("git", ["pull", "origin", ref], + cd: repo_path, + stderr_to_stdout: true + ) + + case status do + 0 -> + IO.puts(output) + repo_path + + _ -> + IO.puts("Warning: Could not pull changes:\n#{output}") + repo_path + end + else + IO.puts("Checked out tag or commit (detached HEAD)") + repo_path + end + end +end diff --git a/lib/mix/tasks/esp32.build.ex b/lib/mix/tasks/esp32.build.ex new file mode 100644 index 0000000..2ebb076 --- /dev/null +++ b/lib/mix/tasks/esp32.build.ex @@ -0,0 +1,462 @@ +defmodule Mix.Tasks.Atomvm.Esp32.Build do + @moduledoc """ + Mix task for building AtomVM for ESP32 from source. + + Builds AtomVM from a local repository or git URL using ESP-IDF. + + ## Requirements + + **General requirements** + * Erlang/OTP (27 or later) + * Elixir (1.18 or later) + * Git + + **Without Docker:** + * CMake (3.13 or later) + * Ninja (preferred) or Make + * ESP-IDF (v5.5.2 recommended) + + **With Docker (--use-docker flag):** + * Docker + * Note: Docker build support requires AtomVM main branch from Jan 2, 2026 or later (https://github.com/atomvm/AtomVM/commit/2a4f0d0fe100ef6d440bef86eabfd08c5b290f6c). + Previous AtomVM versions must be built with the local ESP-IDF toolchain installed. + + ## Options + + * `--atomvm-path` - Path to local AtomVM repository (optional, overrides URL if both provided) + * `--atomvm-url` - Git URL to clone AtomVM from (optional, defaults to AtomVM/AtomVM main branch) + * `--ref` - Git reference to checkout - branch, tag, commit SHA, or PR (e.g. `pr/1234` or `pull/1234/head`) (default: main) + * `--chip` - Target chip(s), comma-separated for multiple (default: esp32, options: esp32, esp32s2, esp32s3, esp32c2, esp32c3, esp32c6, esp32h2, esp32p4) + * `--idf-path` - Path to idf.py executable (default: idf.py) + * `--use-docker` - Use ESP-IDF Docker image instead of local installation + * `--idf-version` - ESP-IDF version for Docker image (default: v5.5.2) + * `--clean` - Clean build directory before building + * `--mbedtls-prefix` - Path to custom MbedTLS installation (optional, falls back to MBEDTLS_PREFIX env var) + + ## Examples + + # Build from local repository + mix atomvm.esp32.build --atomvm-path /path/to/AtomVM + + # Build from git URL + mix atomvm.esp32.build --atomvm-url https://github.com/atomvm/AtomVM --ref main + + # Build from specific tag + mix atomvm.esp32.build --atomvm-url https://github.com/atomvm/AtomVM --ref v0.6.5 + + # Build from specific commit + mix atomvm.esp32.build --atomvm-url https://github.com/atomvm/AtomVM --ref abc123def + + # Build for specific chip with clean build + mix atomvm.esp32.build --atomvm-path /path/to/AtomVM --chip esp32s3 --clean + + # Build using Docker (relative path with ./ is important) + mix atomvm.esp32.build --atomvm-path ./_build/atomvm_source/AtomVM/ --use-docker --chip esp32s3 + + # Build using Docker with specific IDF version + mix atomvm.esp32.build --atomvm-path ./_build/atomvm_source/AtomVM/ --use-docker --idf-version v5.5.2 --chip esp32s3 + + # Build with custom MbedTLS + mix atomvm.esp32.build --atomvm-path /path/to/AtomVM --mbedtls-prefix /usr/local/opt/mbedtls@3 + + # Build from a pull request (shorthand) + mix atomvm.esp32.build --ref pr/1234 + + # Build from a pull request (full refspec) + mix atomvm.esp32.build --ref pull/1234/head --chip esp32s3 + + # Build for multiple chips + mix atomvm.esp32.build --chip esp32,esp32s3,esp32c6 + + """ + use Mix.Task + + @shortdoc "Build AtomVM for ESP32 from source" + + @default_chip "esp32" + @default_ref "main" + @default_atomvm_url "https://github.com/atomvm/AtomVM" + @default_idf_path "idf.py" + @default_idf_version "v5.5.2" + + @impl Mix.Task + def run(args) do + {opts, _} = + OptionParser.parse!(args, + strict: [ + atomvm_path: :string, + atomvm_url: :string, + ref: :string, + chip: :string, + idf_path: :string, + use_docker: :boolean, + idf_version: :string, + clean: :boolean, + mbedtls_prefix: :string + ] + ) + + atomvm_path = Keyword.get(opts, :atomvm_path) + atomvm_url = Keyword.get(opts, :atomvm_url, @default_atomvm_url) + ref = Keyword.get(opts, :ref, @default_ref) + idf_path = Keyword.get(opts, :idf_path, @default_idf_path) + use_docker = Keyword.get(opts, :use_docker, false) + idf_version = Keyword.get(opts, :idf_version, @default_idf_version) + clean = Keyword.get(opts, :clean, false) + + chips = + opts + |> Keyword.get(:chip, @default_chip) + |> String.split(",", trim: true) + |> Enum.map(&String.trim/1) + + # Get mbedtls_prefix from option or environment variable + mbedtls_prefix = + Keyword.get(opts, :mbedtls_prefix) || System.get_env("MBEDTLS_PREFIX") + + # Use --atomvm-path, --atomvm-url, or default to AtomVM/AtomVM main branch + atomvm_path = + cond do + atomvm_path -> + atomvm_path + + true -> + ExAtomVM.AtomVMBuilder.clone_or_update_repo(atomvm_url, ref) + end + + # Verify AtomVM path exists + unless File.dir?(atomvm_path) do + IO.puts("Error: AtomVM path does not exist: #{atomvm_path}") + exit({:shutdown, 1}) + end + + chips_label = Enum.join(chips, ", ") + + IO.puts(""" + + Building AtomVM from source + Repository: #{atomvm_path} + Chip(s): #{chips_label} + Clean build: #{clean} + + """) + + with :ok <- check_esp_idf(idf_path, use_docker, idf_version), + :ok <- ExAtomVM.AtomVMBuilder.build_generic_unix(atomvm_path, mbedtls_prefix, clean) do + results = + chips + |> Enum.with_index(1) + |> Enum.map(fn {chip, index} -> + if length(chips) > 1 do + IO.puts("\n━━━ Building chip #{index}/#{length(chips)}: #{chip} ━━━\n") + end + + force_clean = index > 1 or clean + + case build_atomvm(atomvm_path, chip, idf_path, idf_version, use_docker, force_clean) do + :ok -> + build_dir = Path.join([atomvm_path, "src", "platforms", "esp32", "build"]) + src_img = Path.join([build_dir, "atomvm-#{chip}.img"]) + img = save_image(src_img, chip) + {chip, :ok, img} + + {:error, reason} -> + {chip, :error, reason} + end + end) + + print_summary(results) + + if Enum.any?(results, fn {_, status, _} -> status == :error end) do + exit({:shutdown, 1}) + end + else + {:error, reason} -> + IO.puts("Error: #{reason}") + exit({:shutdown, 1}) + end + end + + defp print_summary(results) do + IO.puts("\n━━━ Build Summary ━━━\n") + + cwd = File.cwd!() + + Enum.each(results, fn + {chip, :ok, img} -> + if File.exists?(img) do + IO.puts(" ✅ #{chip}: #{img}") + else + IO.puts(" ⚠️ #{chip}: built but image not found at #{img}") + end + + {chip, :error, reason} -> + IO.puts(" ❌ #{chip}: #{reason}") + end) + + successful = + Enum.filter(results, fn {_, status, img} -> status == :ok and File.exists?(img) end) + + if successful != [] do + IO.puts("\nTo flash a specific image:") + + Enum.each(successful, fn {_chip, _, img} -> + IO.puts(" mix atomvm.esp32.install --image #{relative_path(img, cwd)}") + end) + end + + IO.puts("") + end + + defp relative_path(path, cwd) do + "./#{Path.relative_to(path, cwd)}" + end + + defp save_image(src_img, chip) do + output_dir = Path.join([File.cwd!(), "_build", "atomvm_images"]) + File.mkdir_p!(output_dir) + dest_img = Path.join(output_dir, "atomvm-#{chip}.img") + + if File.exists?(src_img) do + File.cp!(src_img, dest_img) + dest_img + else + src_img + end + end + + defp check_esp_idf(idf_path, use_docker, idf_version) do + if use_docker do + case System.find_executable("docker") do + nil -> + {:error, + """ + Docker not found. Please install Docker: + + https://docs.docker.com/get-docker/ + """} + + docker_path -> + IO.puts("Found Docker: #{docker_path}") + IO.puts("Using ESP-IDF Docker image: espressif/idf:#{idf_version}") + :ok + end + else + case System.find_executable(idf_path) do + nil -> + {:error, + """ + ESP-IDF not found. Please install and set up ESP-IDF: + + https://docs.espressif.com/projects/esp-idf/en/latest/esp32/get-started/ + + Or use --use-docker to build with Docker instead. + """} + + idf_path_found -> + IO.puts("Found ESP-IDF: #{idf_path_found}") + :ok + end + end + end + + defp configure_elixir_partitions(platform_dir) do + # Per AtomVM docs: Add partition config to sdkconfig.defaults before building + sdkconfig_defaults = platform_dir |> Path.join("sdkconfig.defaults") + + IO.puts("Configuring Elixir partition table (partitions-elixir.csv)...") + + # Read existing defaults or create empty + content = + if File.exists?(sdkconfig_defaults) do + File.read!(sdkconfig_defaults) + else + "" + end + + # Check if partition config already exists + if not String.contains?(content, "CONFIG_PARTITION_TABLE_CUSTOM_FILENAME") do + # Append Elixir partition configuration + new_content = + content <> "\nCONFIG_PARTITION_TABLE_CUSTOM_FILENAME=\"partitions-elixir.csv\"\n" + + File.write!(sdkconfig_defaults, new_content) + IO.puts("✓ Added partitions-elixir.csv to sdkconfig.defaults") + else + # Replace existing config + new_content = + content + |> String.replace( + ~r/CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="[^"]+"/, + ~s(CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="partitions-elixir.csv") + ) + + File.write!(sdkconfig_defaults, new_content) + IO.puts("✓ Updated sdkconfig.defaults to use partitions-elixir.csv") + end + end + + defp build_atomvm(atomvm_path, chip, idf_path, idf_version, use_docker, clean) do + build_dir = Path.join([atomvm_path, "src", "platforms", "esp32", "build"]) + platform_dir = Path.join([atomvm_path, "src", "platforms", "esp32"]) + + # Copy idf_component.yml into build tree if the user has one in their project root + idf_component_yml = Path.join(File.cwd!(), "idf_component.yml") + + idf_component_example = Path.join(File.cwd!(), "idf_component.yml.example") + + if File.exists?(idf_component_yml) do + dest_path = Path.join([platform_dir, "main", "idf_component.yml"]) + IO.puts("Copying idf_component.yml to #{dest_path}...") + File.cp!(idf_component_yml, dest_path) + else + unless File.exists?(idf_component_example) do + example_src = Application.app_dir(:exatomvm, "priv/idf_component.yml.example") + File.cp!(example_src, idf_component_example) + end + + IO.puts( + "Hint: To add ESP-IDF components (e.g. NIFs), rename the example in your project root:\n" <> + " mv idf_component.yml.example idf_component.yml" + ) + end + + # Copy dependencies.lock if it exists in the project root + dependencies_lock = Path.join(File.cwd!(), "dependencies.lock") + + if File.exists?(dependencies_lock) do + dest_path = Path.join(platform_dir, "dependencies.lock") + IO.puts("Copying dependencies.lock to #{dest_path}...") + File.cp!(dependencies_lock, dest_path) + end + + if clean and File.dir?(build_dir) do + IO.puts("Cleaning build directory...") + File.rm_rf!(build_dir) + end + + IO.puts("Configuring build for #{chip}...") + + # Configure Elixir partition table in sdkconfig.defaults BEFORE set-target + configure_elixir_partitions(platform_dir) + + # Set target chip + {_output, status} = + if use_docker do + run_idf_docker(idf_version, atomvm_path, platform_dir, ["set-target", chip]) + else + System.cmd(idf_path, ["set-target", chip], + cd: platform_dir, + stderr_to_stdout: true, + into: IO.stream(:stdio, :line) + ) + end + + case status do + 0 -> + # Reconfigure to ensure partition table settings are applied + IO.puts("Reconfiguring to apply Elixir partitions...") + + {_output, status} = + if use_docker do + run_idf_docker(idf_version, atomvm_path, platform_dir, ["reconfigure"]) + else + System.cmd(idf_path, ["reconfigure"], + cd: platform_dir, + stderr_to_stdout: true, + into: IO.stream(:stdio, :line) + ) + end + + status = + case status do + 0 -> + IO.puts("Building AtomVM... (this may take several minutes)") + + {_output, build_status} = + if use_docker do + run_idf_docker(idf_version, atomvm_path, platform_dir, ["build"]) + else + System.cmd(idf_path, ["build"], + cd: platform_dir, + stderr_to_stdout: true, + into: IO.stream(:stdio, :line) + ) + end + + if build_status == 0 do + # Copy dependencies.lock back to project root if it was created/updated + repo_dependencies_lock = Path.join(platform_dir, "dependencies.lock") + + if File.exists?(repo_dependencies_lock) do + dest_path = Path.join(File.cwd!(), "dependencies.lock") + IO.puts("Updating dependencies.lock in project root...") + File.cp!(repo_dependencies_lock, dest_path) + end + end + + build_status + + _ -> + status + end + + case status do + 0 -> + # Use absolute paths to avoid issues with relative paths + abs_atomvm_path = Path.expand(atomvm_path) + abs_build_dir = Path.expand(build_dir) + mkimage_script = Path.join([abs_build_dir, "mkimage.sh"]) + + IO.puts("Creating flashable image...") + # TODO: Remove --boot flag when AtomVM#1163 is merged + boot_avm = + Path.join([abs_atomvm_path, "build", "libs", "esp32boot", "elixir_esp32boot.avm"]) + + {_output, status} = + System.cmd("sh", [mkimage_script, "--boot", boot_avm], + cd: abs_build_dir, + stderr_to_stdout: true, + into: IO.stream(:stdio, :line) + ) + + case status do + 0 -> + :ok + + _ -> + {:error, "Failed to create image"} + end + + _ -> + {:error, "Build failed"} + end + + _ -> + {:error, "Failed to set target chip"} + end + end + + defp run_idf_docker(idf_version, atomvm_path, platform_dir, idf_args) do + # Calculate the relative path from atomvm_path to platform_dir + relative_dir = Path.relative_to(platform_dir, atomvm_path) + + # Build docker command + docker_args = + [ + "run", + "--rm", + "-v", + "#{atomvm_path}:/project", + "-w", + "/project/#{relative_dir}", + "espressif/idf:#{idf_version}", + "idf.py" + ] ++ idf_args + + System.cmd("docker", docker_args, + stderr_to_stdout: true, + into: IO.stream(:stdio, :line) + ) + end +end diff --git a/lib/mix/tasks/esp32.install.ex b/lib/mix/tasks/esp32.install.ex index 5974f2f..b5fb7ac 100644 --- a/lib/mix/tasks/esp32.install.ex +++ b/lib/mix/tasks/esp32.install.ex @@ -1,22 +1,52 @@ defmodule Mix.Tasks.Atomvm.Esp32.Install do @moduledoc """ - Mix task for erasing flash and installing the latest AtomVM release to connected device. + Mix task for erasing flash and installing AtomVM to connected device. - Takes an optional --baud option to set the baud rate of the flashing. - Defaults to 921600, use 115200 for slower devices. + By default, downloads and installs the latest AtomVM release from GitHub. + Optionally, can install a custom-built image using the --image option. + + **WARNING:** This task erases the current flash before installing. + + ## Options + + * `--image` - Path to custom AtomVM .img file (optional, downloads latest release if not provided) + * `--baud` - Baud rate for flashing (default: 921600, use 115200 for slower devices) + + ## Examples + + # Install latest release from GitHub (erases flash) + mix atomvm.esp32.install + + # Install custom-built image (erases flash) + mix atomvm.esp32.install --image /path/to/AtomVM-esp32s3.img + + # Install with custom baud rate + mix atomvm.esp32.install --baud 115200 After install, your project can be flashed with: mix atomvm.esp32.flash """ use Mix.Task - @shortdoc "Install latest AtomVM release on ESP32" + @shortdoc "Install AtomVM to ESP32 device" alias ExAtomVM.EsptoolHelper @impl Mix.Task def run(args) do - {opts, _} = OptionParser.parse!(args, strict: [baud: :string]) + {opts, _} = OptionParser.parse!(args, strict: [image: :string, baud: :string]) + + case Keyword.get(opts, :image) do + nil -> + run_with_latest_release(opts) + + image_path -> + run_with_custom_image(image_path, opts) + end + end + + # Install latest release from GitHub + defp run_with_latest_release(opts) do baud = Keyword.get(opts, :baud, "921600") with :ok <- check_req_dependency(), @@ -58,13 +88,51 @@ defmodule Mix.Tasks.Atomvm.Esp32.Install do end end + # Install custom-built image + defp run_with_custom_image(image_path, opts) do + baud = Keyword.get(opts, :baud, "921600") + + if not File.exists?(image_path) do + IO.puts("Error: Image file not found: #{image_path}") + exit({:shutdown, 1}) + end + + with :ok <- EsptoolHelper.setup(), + selected_device <- EsptoolHelper.select_device(), + :ok <- confirm_erase_and_flash(selected_device, image_path), + true <- + EsptoolHelper.erase_flash([ + "--port", + selected_device["port"], + "--chip", + "auto", + "--after", + "no-reset" + ]), + :timer.sleep(3000), + true <- flash_release(selected_device, image_path, baud) do + IO.puts(""" + + Successfully installed AtomVM on #{selected_device["chip_family_name"]} Port: #{selected_device["port"]} MAC: #{selected_device["mac_address"]} + + Your project can be flashed with: + mix atomvm.esp32.flash + + """) + else + {:error, reason} -> + IO.puts("Error: #{reason}") + exit({:shutdown, 1}) + end + end + defp confirm_erase_and_flash(selected_device, release_file) do confirmation = IO.gets(""" Are you sure you want to erase the flash of #{selected_device["chip_family_name"]} - Port: #{selected_device["port"]} MAC: #{selected_device["mac_address"]} - And install AtomVM: #{release_file} + And install AtomVM: #{Path.basename(release_file)} ? [N/y]: """) @@ -149,7 +217,9 @@ defmodule Mix.Tasks.Atomvm.Esp32.Install do "ESP32-S3" => "0x0", "ESP32-C2" => "0x0", "ESP32-C3" => "0x0", + "ESP32-C5" => "0x2000", "ESP32-C6" => "0x0", + "ESP32-C61" => "0x0", "ESP32-H2" => "0x0", "ESP32-P4" => "0x2000" }[device["chip_family_name"]] || "0x0" diff --git a/priv/idf_component.yml.example b/priv/idf_component.yml.example new file mode 100644 index 0000000..92c5a8f --- /dev/null +++ b/priv/idf_component.yml.example @@ -0,0 +1,59 @@ +# IDF component manifest for AtomVM on ESP32. +# +# To use this file: +# 1. Uncomment the dependencies you need below. +# 2. Rename it: mv idf_component.yml.example idf_component.yml +# 3. Rebuild: mix atomvm.esp32.build ... +# +# Note: At least one dependency must be uncommented before renaming, +# otherwise the empty `dependencies:` key will cause a build error. +# comment the `dependencies:` for no dependencies +# +# This file is copied into the AtomVM source tree at: +# /src/platforms/esp32/main/idf_component.yml +# +# Relative `path:` entries are resolved from that location. +# +# Without Docker: +# Paths are relative to /src/platforms/esp32/main/ +# e.g. path: ../../../../../my_nif +# navigates up from main/ -> esp32/ -> platforms/ -> src/ -> AtomVM/ -> parent +# +# With Docker (--use-docker): +# Only the AtomVM repository is mounted in the container. +# Local path dependencies outside the AtomVM tree are NOT accessible. +# +# Read more on the IDF component manager: +# https://docs.espressif.com/projects/esp-idf/en/stable/esp32/api-guides/tools/idf-component-manager.html +# +# Check the readme for each dependency for more information on install and usage. +# +dependencies: + # atomgl: + # git: https://github.com/atomvm/atomgl + # version: "main" + # atomvm_mqtt_client: + # git: https://github.com/atomvm/atomvm_mqtt_client + # version: "main" + # atomvm_esp32cam: + # # NB requires certain sdkconfig options to be set, see the readme + # git: https://github.com/atomvm/atomvm_esp32cam + # version: "main" + # atomvm_gps: + # git: https://github.com/atomvm/atomvm_gps + # version: "main" + # atomvm_neopixel: + # git: https://github.com/atomvm/atomvm_neopixel + # version: "main" + # atomvm_m5: + # git: https://github.com/pguyot/atomvm_m5 + # version: "main" + # + # --- Local path examples (non-Docker only) --- + # + # my_nif: + # # local NIF in a sibling directory next to AtomVM + # path: ../../../../../my_nif + # atomvm_mqtt_client: + # # git-cloned NIF in a sibling directory next to AtomVM + # path: ../../../../../atomvm_mqtt_client