From b575c181dbcec092baa65436a042732d5d93d18b Mon Sep 17 00:00:00 2001 From: Frank Hunleth Date: Sat, 16 Aug 2025 12:52:00 -0400 Subject: [PATCH] Support builds that completely exclude I2CDev support This removes both the Elixir module and the NIF. There's no longer any unintended NIF builds or loads or anything extra. To enable this, either set the `:default_backend` application environment key or force it by setting `:include_i2c_dev`. Most users probably want: ```elixir config :circuits_i2c, default_backend: MyI2CBackend ``` --- .formatter.exs | 2 +- Makefile | 15 ++-- README.md | 21 +++++- REUSE.toml | 2 +- {c_src => i2c_dev/c_src}/compat/linux/types.h | 0 {c_src => i2c_dev/c_src}/i2c_nif.c | 0 {c_src => i2c_dev/c_src}/linux/i2c-dev.h | 0 {lib => i2c_dev/lib}/i2c/i2c_dev.ex | 0 {lib => i2c_dev/lib}/i2c/i2c_nif.ex | 0 lib/i2c/nil_backend.ex | 26 +++++++ mix.exs | 72 ++++++++++++------- mix.lock | 2 +- 12 files changed, 103 insertions(+), 37 deletions(-) rename {c_src => i2c_dev/c_src}/compat/linux/types.h (100%) rename {c_src => i2c_dev/c_src}/i2c_nif.c (100%) rename {c_src => i2c_dev/c_src}/linux/i2c-dev.h (100%) rename {lib => i2c_dev/lib}/i2c/i2c_dev.ex (100%) rename {lib => i2c_dev/lib}/i2c/i2c_nif.ex (100%) diff --git a/.formatter.exs b/.formatter.exs index 5a0d2db..b8a2b06 100644 --- a/.formatter.exs +++ b/.formatter.exs @@ -1,4 +1,4 @@ # Used by "mix format" [ - inputs: ["{mix,.formatter,.credo}.exs", "{config,lib,test}/**/*.{ex,exs}"] + inputs: ["{mix,.formatter,.credo}.exs", "{config,i2c_dev,lib,test}/**/*.{ex,exs}"] ] diff --git a/Makefile b/Makefile index 0e3768b..c524625 100644 --- a/Makefile +++ b/Makefile @@ -14,7 +14,7 @@ # Variables to override: # # MIX_APP_PATH path to the build directory -# CIRCUITS_I2C_I2CDEV Backend to build - `"normal"`, `"test"`, or `"disabled"` will build a NIF +# CIRCUITS_I2C_I2CDEV Backend to build - `"normal"` or `"test"` # # CC C compiler # CROSSCOMPILE crosscompiler prefix, if any @@ -30,7 +30,7 @@ BUILD = $(MIX_APP_PATH)/obj NIF = $(PREFIX)/i2c_nif.so -SRC = c_src/i2c_nif.c +SRC = i2c_dev/c_src/i2c_nif.c CFLAGS ?= -O2 -Wall -Wextra -Wno-unused-parameter -pedantic # Check that we're on a supported build platform @@ -40,7 +40,7 @@ ifeq ($(shell uname -s),Linux) CFLAGS += -fPIC LDFLAGS += -fPIC -shared else -CFLAGS += -Ic_src/compat +CFLAGS += -Ii2c_dev/c_src/compat LDFLAGS += -undefined dynamic_lookup -dynamiclib ifeq ($(CIRCUITS_I2C_I2CDEV),normal) $(error Circuits.I2C Linux I2CDev backend is not supported on non-Linux platforms. Review circuits_i2c backend configuration or report an issue if improperly detected.) @@ -59,8 +59,7 @@ ifeq ($(CIRCUITS_I2C_I2CDEV),test) # Stub out ioctls and send back test data CFLAGS += -DTEST_BACKEND else -# Don't build NIF -NIF = +$(error Invalid CIRCUITS_I2C_I2CDEV value: $(CIRCUITS_I2C_I2CDEV)) endif endif @@ -68,8 +67,8 @@ endif ERL_CFLAGS ?= -I"$(ERL_EI_INCLUDE_DIR)" ERL_LDFLAGS ?= -L"$(ERL_EI_LIBDIR)" -lei -HEADERS =$(wildcard c_src/*.h) -OBJ = $(SRC:c_src/%.c=$(BUILD)/%.o) +HEADERS =$(wildcard i2c_dev/c_src/*.h) +OBJ = $(SRC:i2c_dev/c_src/%.c=$(BUILD)/%.o) calling_from_make: mix compile @@ -80,7 +79,7 @@ install: $(PREFIX) $(BUILD) $(NIF) $(OBJ): $(HEADERS) Makefile -$(BUILD)/%.o: c_src/%.c +$(BUILD)/%.o: i2c_dev/c_src/%.c @echo " CC $(notdir $@)" $(CC) -c $(ERL_CFLAGS) $(CFLAGS) -o $@ $< diff --git a/README.md b/README.md index e3d751e..d634158 100644 --- a/README.md +++ b/README.md @@ -48,7 +48,8 @@ may be helpful. Internally, it uses the [Linux "i2c-dev" interface](https://elixir.bootlin.com/linux/latest/source/Documentation/i2c/dev-interface) -so that it does not require board-dependent code. +so that it does not require board-dependent code. See the application +configuration for disabling all things Linux/Nerves specific. ## Getting started without hardware @@ -56,6 +57,24 @@ If you don't have any real I2C devices, it's possible to work with simulated devices. See the [CircuitsSim](https://github.com/elixir-circuits/circuits_sim) project for details. +## Application config + +`Circuits.I2C` supports the following application environment keys: + +| Key | Description | +| --- | ----------- | +| `:include_i2c_dev` | Set to `true` or `false` to indicate whether or not to include the `Circuits.I2C.I2CDev` backend. | +| `:default_backend` | Default I2C backend to use when unspecified in API calls. The format is either a module name or a `{module_name, options}` tuple. | + +`Circuits.I2C` uses a heuristic for the default values of both +`:include_i2c_dev`. For example, if the `:default_backend` is set to +`Circuits.I2C.I2CDev`, then `:include_i2c_dev` has to be `true`. If +`:default_backend` is set to something else, then `:include_i2c_dev` defaults to +`false`. If neither is set, then `Circuits.I2C.I2CDev` is built. On non-Linux +platforms, the `Circuits.I2C.I2CDev` NIF will be compiled in test mode which +minimally supports unit testing. Mocking is generally a better option for most +users, though. + ## I2C background An [Inter-Integrated Circuit](https://en.wikipedia.org/wiki/I%C2%B2C) (I2C) bus diff --git a/REUSE.toml b/REUSE.toml index 58c7c34..e160407 100644 --- a/REUSE.toml +++ b/REUSE.toml @@ -28,7 +28,7 @@ SPDX-FileCopyrightText = "2014 Frank Hunleth" SPDX-License-Identifier = "CC-BY-4.0" [[annotations]] -path = ["c_src/linux/i2c-dev.h"] +path = ["i2c_dev/c_src/linux/i2c-dev.h"] precedence = "aggregate" SPDX-FileCopyrightText = "None" SPDX-License-Identifier = "GPL-2.0-or-later WITH Linux-syscall-note" diff --git a/c_src/compat/linux/types.h b/i2c_dev/c_src/compat/linux/types.h similarity index 100% rename from c_src/compat/linux/types.h rename to i2c_dev/c_src/compat/linux/types.h diff --git a/c_src/i2c_nif.c b/i2c_dev/c_src/i2c_nif.c similarity index 100% rename from c_src/i2c_nif.c rename to i2c_dev/c_src/i2c_nif.c diff --git a/c_src/linux/i2c-dev.h b/i2c_dev/c_src/linux/i2c-dev.h similarity index 100% rename from c_src/linux/i2c-dev.h rename to i2c_dev/c_src/linux/i2c-dev.h diff --git a/lib/i2c/i2c_dev.ex b/i2c_dev/lib/i2c/i2c_dev.ex similarity index 100% rename from lib/i2c/i2c_dev.ex rename to i2c_dev/lib/i2c/i2c_dev.ex diff --git a/lib/i2c/i2c_nif.ex b/i2c_dev/lib/i2c/i2c_nif.ex similarity index 100% rename from lib/i2c/i2c_nif.ex rename to i2c_dev/lib/i2c/i2c_nif.ex diff --git a/lib/i2c/nil_backend.ex b/lib/i2c/nil_backend.ex index 528eb8d..dd0562f 100644 --- a/lib/i2c/nil_backend.ex +++ b/lib/i2c/nil_backend.ex @@ -9,6 +9,9 @@ defmodule Circuits.I2C.NilBackend do @behaviour Circuits.I2C.Backend alias Circuits.I2C.Backend + alias Circuits.I2C.Bus + + defstruct [] @doc """ Return the I2C bus names on this system @@ -35,4 +38,27 @@ defmodule Circuits.I2C.NilBackend do def info() do %{name: __MODULE__} end + + defimpl Bus do + @impl Bus + def flags(%Circuits.I2C.NilBackend{}), do: [] + + @impl Bus + def read(%Circuits.I2C.NilBackend{}, _address, _count, _options) do + {:error, :unimplemented} + end + + @impl Bus + def write(%Circuits.I2C.NilBackend{}, _address, _data, _options) do + {:error, :unimplemented} + end + + @impl Bus + def write_read(%Circuits.I2C.NilBackend{}, _address, _write_data, _read_count, _options) do + {:error, :unimplemented} + end + + @impl Bus + def close(%Circuits.I2C.NilBackend{}), do: :ok + end end diff --git a/mix.exs b/mix.exs index 658b0e3..098b75c 100644 --- a/mix.exs +++ b/mix.exs @@ -6,47 +6,54 @@ defmodule Circuits.I2C.MixProject do @source_url "https://github.com/elixir-circuits/circuits_i2c" def project do - [ + base = [ app: :circuits_i2c, version: @version, elixir: "~> 1.13", description: @description, package: package(), source_url: @source_url, - compilers: [:elixir_make | Mix.compilers()], - elixirc_paths: elixirc_paths(Mix.env()), - make_targets: ["all"], - make_clean: ["clean"], docs: docs(), - aliases: [compile: [&set_make_env/1, "compile"], format: [&format_c/1, "format"]], start_permanent: Mix.env() == :prod, dialyzer: [ flags: [:missing_return, :extra_return, :unmatched_returns, :error_handling, :underspecs] ], deps: deps() ] + + if build_i2c_dev?() do + additions = [ + compilers: [:elixir_make | Mix.compilers()], + elixirc_paths: ["lib", "i2c_dev/lib"], + make_targets: ["all"], + make_clean: ["clean"], + aliases: [compile: [&set_make_env/1, "compile"], format: [&format_c/1, "format"]] + ] + + Keyword.merge(base, additions) + else + base + end end def cli do [preferred_envs: %{docs: :docs, "hex.publish": :docs, "hex.build": :docs}] end - defp elixirc_paths(env) when env in [:test, :dev], do: ["lib", "examples"] - defp elixirc_paths(_env), do: ["lib"] - def application do - # IMPORTANT: This provides a default at runtime and at compile-time when + # IMPORTANT: This provides defaults at runtime and at compile-time when # circuits_i2c is pulled in as a dependency. - [env: [default_backend: default_backend()]] + [env: [default_backend: default_backend(), build_i2c_dev: false]] end defp package do %{ files: [ "CHANGELOG.md", - "c_src/*.[ch]", - "c_src/linux/*.h", - "c_src/compat/linux/*.h", + "i2c_dev/c_src/*.[ch]", + "i2c_dev/c_src/linux/*.h", + "i2c_dev/c_src/compat/linux/*.h", + "i2c_dev/lib", "lib", "LICENSES", "Makefile", @@ -85,24 +92,43 @@ defmodule Circuits.I2C.MixProject do ] end - defp default_backend(), do: default_backend(Mix.env(), Mix.target()) - defp default_backend(:test, _target), do: {Circuits.I2C.I2CDev, test: true} + defp build_i2c_dev?() do + include_i2c_dev = Application.get_env(:circuits_i2c, :include_i2c_dev) + + if include_i2c_dev != nil do + # If the user set :include_i2c_dev, then use it + include_i2c_dev + else + # Otherwise, infer whether to build it based on the default_backend + # setting. If default_backend references it, then build it. If it + # references something else, then don't build. Default is to build. + default_backend = Application.get_env(:circuits_i2c, :default_backend) - defp default_backend(_env, :host) do + default_backend == nil or default_backend == Circuits.I2C.I2CDev or + (is_tuple(default_backend) and elem(default_backend, 0) == Circuits.I2C.I2CDev) + end + end + + defp default_backend(), do: default_backend(Mix.env(), Mix.target(), build_i2c_dev?()) + defp default_backend(:test, _target, true), do: {Circuits.I2C.I2CDev, test: true} + + defp default_backend(_env, :host, true) do case :os.type() do {:unix, :linux} -> Circuits.I2C.I2CDev _ -> {Circuits.I2C.I2CDev, test: true} end end + defp default_backend(_env, _target, false), do: Circuits.I2C.NilBackend + # MIX_TARGET set to something besides host - defp default_backend(env, _not_host) do + defp default_backend(env, _not_host, true) do # If CROSSCOMPILE is set, then the Makefile will use the crosscompiler and # assume a Linux/Nerves build If not, then the NIF will be build for the # host, so use the default host backend case System.fetch_env("CROSSCOMPILE") do {:ok, _} -> Circuits.I2C.I2CDev - :error -> default_backend(env, :host) + :error -> default_backend(env, :host, true) end end @@ -123,12 +149,8 @@ defmodule Circuits.I2C.MixProject do end end - defp i2c_dev_compile_mode(Circuits.I2C.I2CDev) do - "normal" - end - defp i2c_dev_compile_mode(_other) do - "disabled" + "normal" end defp format_c([]) do @@ -137,7 +159,7 @@ defmodule Circuits.I2C.MixProject do Mix.Shell.IO.info("Install astyle to format C code.") astyle -> - System.cmd(astyle, ["-n", "c_src/*.c"], into: IO.stream(:stdio, :line)) + System.cmd(astyle, ["-n", "i2c_dev/c_src/*.c"], into: IO.stream(:stdio, :line)) end end diff --git a/mix.lock b/mix.lock index 2f28791..feb5938 100644 --- a/mix.lock +++ b/mix.lock @@ -2,7 +2,7 @@ "bunt": {:hex, :bunt, "1.0.0", "081c2c665f086849e6d57900292b3a161727ab40431219529f13c4ddcf3e7a44", [:mix], [], "hexpm", "dc5f86aa08a5f6fa6b8096f0735c4e76d54ae5c9fa2c143e5a1fc7c1cd9bb6b5"}, "credo": {:hex, :credo, "1.7.12", "9e3c20463de4b5f3f23721527fcaf16722ec815e70ff6c60b86412c695d426c1", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "8493d45c656c5427d9c729235b99d498bd133421f3e0a683e5c1b561471291e5"}, "credo_binary_patterns": {:hex, :credo_binary_patterns, "0.2.6", "cfcaca0bc5c6447b96c5a03eff175c28f86c486be8e95d55b360fb90c2dd18bd", [:mix], [{:credo, "~> 1.6", [hex: :credo, repo: "hexpm", optional: false]}], "hexpm", "d36a2b56ad72bdf3183ccc81d7e7821e78c97de7c127bc8dd99a5f05ca702187"}, - "dialyxir": {:hex, :dialyxir, "1.4.5", "ca1571ac18e0f88d4ab245f0b60fa31ff1b12cbae2b11bd25d207f865e8ae78a", [:mix], [{:erlex, ">= 0.2.7", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "b0fb08bb8107c750db5c0b324fa2df5ceaa0f9307690ee3c1f6ba5b9eb5d35c3"}, + "dialyxir": {:hex, :dialyxir, "1.4.6", "7cca478334bf8307e968664343cbdb432ee95b4b68a9cba95bdabb0ad5bdfd9a", [:mix], [{:erlex, ">= 0.2.7", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "8cf5615c5cd4c2da6c501faae642839c8405b49f8aa057ad4ae401cb808ef64d"}, "earmark_parser": {:hex, :earmark_parser, "1.4.44", "f20830dd6b5c77afe2b063777ddbbff09f9759396500cdbe7523efd58d7a339c", [:mix], [], "hexpm", "4778ac752b4701a5599215f7030989c989ffdc4f6df457c5f36938cc2d2a2750"}, "elixir_make": {:hex, :elixir_make, "0.9.0", "6484b3cd8c0cee58f09f05ecaf1a140a8c97670671a6a0e7ab4dc326c3109726", [:mix], [], "hexpm", "db23d4fd8b757462ad02f8aa73431a426fe6671c80b200d9710caf3d1dd0ffdb"}, "erlex": {:hex, :erlex, "0.2.7", "810e8725f96ab74d17aac676e748627a07bc87eb950d2b83acd29dc047a30595", [:mix], [], "hexpm", "3ed95f79d1a844c3f6bf0cea61e0d5612a42ce56da9c03f01df538685365efb0"},