diff --git a/.github/workflows/build-and-test-elixir.yaml b/.github/workflows/build-and-test-elixir.yaml new file mode 100644 index 0000000..5f5d120 --- /dev/null +++ b/.github/workflows/build-and-test-elixir.yaml @@ -0,0 +1,79 @@ +# +# Copyright 2022 Fred Dushin +# +# SPDX-License-Identifier: Apache-2.0 OR LGPL-2.1-or-later +# + +name: Build and Test Elixir PackBEAM.API + +on: + push: + paths: + - "mix.exs" + - "lib/**/*.exs" + - "lib/**/*.ex" + - "test/**/*.exs" + - "test/**/*.ex" + pull_request: + paths: + - "mix.exs" + - "lib/**/*.ex" + - "lib/**/*.exs" + - "test/**/*.exs" + - "test/**/*.ex" + workflow_dispatch: + +env: + ELIXIR_ASSERT_TIMEOUT: 2000 + ELIXIRC_OPTS: "--warnings-as-errors" + LANG: C.UTF-8 + +permissions: + contents: read + +jobs: + build-and-test-elixir: + runs-on: "ubuntu-24.04" + strategy: + fail-fast: false + matrix: + # otp: ["25", "26", "27", "28"] + include: + - otp: "25" + elixir_version: "1.14" + - otp: "26" + elixir_version: "1.17" + - otp: "27" + elixir_version: "1.18" + - otp: "28" + elixir_version: "1.19" + + steps: + # Setup + - name: "Checkout repo" + uses: actions/checkout@v2 + with: + submodules: 'recursive' + + - uses: erlef/setup-beam@v1 + with: + otp-version: ${{ matrix.otp }} + elixir-version: ${{ matrix.elixir_version }} + + # Builder info + - name: "System info" + run: | + echo "**uname:**" + uname -a + echo "**OTP version:**" + cat $(dirname $(which erlc))/../releases/RELEASES || true + echo "**Elixir version:**" + elixir --version + + # Build + - name: "Compile with mix" + run: mix compile + + # Test + - name: "Run tests with mix" + run: mix test diff --git a/lib/packbeam.ex b/lib/packbeam.ex new file mode 100644 index 0000000..91ad807 --- /dev/null +++ b/lib/packbeam.ex @@ -0,0 +1,148 @@ +# +# This file is part of atomvm_packbeam. +# +# Copyright 2025 Winford (Uncle Grumpy) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 OR LGPL-2.1-or-later +# + +defmodule PackBEAM.API do + @moduledoc """ + A library used to generate an + AtomVM AVM file from a set of + files (beam files, previously built AVM files, or even arbitrary data files). + + This is an Elixir interface for the native Erlang packbeam_api library. + """ + + @type path :: path + @type avm_element :: [atom | {atom, term}] + @type avm_element_name :: String.t() + @type options :: %{ + prune: prune :: boolean, + start_module: start :: atom, + include_lines: lines :: boolean, + arch: arch :: atom, + platform: platform :: atom + } + + @doc """ + Create an AVM file with default options. + + Equivalent to `create(outpath, inputpaths, defaultopts)` + + where `defaultopts` is `#%{ + :prune => false, + :start_module => :undefined, + :application_module => :undefined, + :include_lines => false, + :arch => :undefined, + :platform => :generic_unix + }` + """ + @spec create(outpath :: path, inputpaths :: [path]) :: :ok | {:error, reason :: term} + def create(outpath, inputpaths) do + :packbeam_api.create(String.to_charlist(outpath), String.to_charlist(inputpaths)) + end + + @doc """ + Create an AVM file. + + This function will create an AVM file at the location specified in `outpath`, using the input + files specified in `inputpaths` using the specified `options`. + """ + @spec create(outpath :: path, inputpaths :: [path], options :: options) :: + :ok | {:error, reason :: term} + def create(outpath, inputpaths, options) do + :packbeam_api.create(String.to_charlist(outpath), inputpaths, options) + end + + @doc """ + List the contents of an AVM. + """ + @spec list(inputpath :: path) :: [avm_element] + def list(inputpath) do + :packbeam_api.list(String.to_charlist(inputpath)) + end + + @doc """ + Extract all or selected elements from an AVM file. + + This function will extract elements of an AVM file at the location specified in `inputpath`, + specified by the supplied list of names. The elements from the input AVM file will be written + into the specified output directory, creating any subdirectories if the AVM file elements contain + path information. + """ + @spec extract(inputpath :: path, amvelements :: [avm_element_name], outdir :: path) :: + :ok | {:error, reason :: term} + def extract(inputpath, amvelements, outdir) do + elements = List.foldl(amvelements, [], fn x, acc -> [String.to_charlist(x) | acc] end) + :packbeam_api.extract(inputpath, elements, outdir) + end + + @doc """ + Delete selected elements of an AVM file. + + This function will delete elements of an AVM file at the location specified in `inputpath`, + specified by the supplied list of names. The output AVM file is written to `outpath`, which may + be the same as `inputpath`. + """ + @spec delete(outpath :: path, inputpath :: path, amvelements :: [avm_element_name]) :: + :ok | {:error, reason :: term} + def delete(outpath, inputpath, amvelements) do + elements = List.foldl(amvelements, [], fn x, acc -> [String.to_charlist(x) | acc] end) + :packbeam_api.delete(String.to_charlist(outpath), inputpath, String.to_charlist(elements)) + end + + @doc """ + Return the name of the element. + """ + @spec get_element_name(amvelement :: avm_element) :: atom + def get_element_name(amvelement) do + :packbeam_api.get_element_name(String.to_charlist(amvelement)) + end + + @doc """ + Return the AVM element data. + """ + @spec get_element_data(amvelement :: avm_element) :: binary + def get_element_data(amvelement) do + :packbeam_api.get_element_data(String.to_charlist(amvelement)) + end + + @doc """ + Return AVM element module, if the element is a BEAM file. + """ + @spec get_element_module(amvelement :: avm_element) :: module | :undefined + def get_element_module(amvelement) do + :packbeam_api.get_element_module(String.to_charlist(amvelement)) + end + + @doc """ + Indicates whether the AVM file element is an entrypoint. + """ + @spec is_entrypoint(amvelement :: avm_element) :: boolean + def is_entrypoint(amvelement) do + :packbeam_api.is_entrypoint(String.to_charlist(amvelement)) + end + + @doc """ + Indicates whether the AVM file element is a BEAM file. + """ + @spec is_beam(amvelement :: avm_element) :: boolean + def is_beam(amvelement) do + :packbeam_api.is_beam(String.to_charlist(amvelement)) + end +end diff --git a/mix.exs b/mix.exs new file mode 100644 index 0000000..710a9b1 --- /dev/null +++ b/mix.exs @@ -0,0 +1,74 @@ +# +# This file is part of atomvm_packbeam. +# +# Copyright 2025 Winford (Uncle Grumpy) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 OR LGPL-2.1-or-later +# + +defmodule Packbeam.MixProject do + use Mix.Project + + def project do + [ + app: :packbeam, + version: "0.7.5", + elixir: "~> 1.7", + start_permanent: Mix.env() == :prod, + deps: deps(), + + # Docs + name: "Packbeam", + source_url: "https://github.com/atomvm/atomvm_packbeam", + homepage_url: "https://www.atomvm.org/", + docs: [ + # The main page in the docs + main: "README.md", + skip_undefined_reference_warnings_on: "README.md", + api_reference: true, + output: "elixir_docs", + extras: [ + "README.md", + "CHANGELOG.md", + "UPDATING.md", + "LICENSE", + "CONTRIBUTING.md", + "CODE_OF_CONDUCT.md" + ] + ], + + # Tests + elixirc_paths: elixirc_paths(Mix.env()) + ] + end + + defp elixirc_paths(:test), do: ["lib", "test"] + defp elixirc_paths(_), do: ["lib"] + + # Run "mix help compile.app" to learn about applications. + def application do + [ + extra_applications: [:logger] + ] + end + + # Run "mix help deps" to learn about dependencies. + defp deps do + [ + # TODO: add property tests + # {:propcheck, "~> 1.4", only: [:test, :dev]} + ] + end +end diff --git a/test/a.ex b/test/a.ex new file mode 100644 index 0000000..cfec00b --- /dev/null +++ b/test/a.ex @@ -0,0 +1,29 @@ +# +# This file is part of atomvm_packbeam. +# +# Copyright 2025 Winford (Uncle Grumpy) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 OR LGPL-2.1-or-later +# + +defmodule A do + @moduledoc """ + Test module for TestPackbeam + """ + + def start do + B.start() + end +end diff --git a/test/b.ex b/test/b.ex new file mode 100644 index 0000000..bae33bf --- /dev/null +++ b/test/b.ex @@ -0,0 +1,35 @@ +# +# This file is part of atomvm_packbeam. +# +# Copyright 2025 Winford (Uncle Grumpy) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 OR LGPL-2.1-or-later +# + +defmodule B do + @moduledoc """ + Test module for TestPackbeam + """ + + def start do + E.b_calls_me() + module = get_module() + module.test() + end + + defp get_module() do + C + end +end diff --git a/test/c.ex b/test/c.ex new file mode 100644 index 0000000..d202829 --- /dev/null +++ b/test/c.ex @@ -0,0 +1,35 @@ +# +# This file is part of atomvm_packbeam. +# +# Copyright 2025 Winford (Uncle Grumpy) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 OR LGPL-2.1-or-later +# + +defmodule C do + @moduledoc """ + Test module for TestPackbeam + """ + + def test do + literal = get_literal() + module = :maps.get(:module, literal) + module.c_calls_me() + end + + defp get_literal() do + %{:module => F} + end +end diff --git a/test/d.ex b/test/d.ex new file mode 100644 index 0000000..2c69bfa --- /dev/null +++ b/test/d.ex @@ -0,0 +1,29 @@ +# +# This file is part of atomvm_packbeam. +# +# Copyright 2025 Winford (Uncle Grumpy) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 OR LGPL-2.1-or-later +# + +defmodule D do + @moduledoc """ + Test module for TestPackbeam + """ + + def no_one_calls_me do + :sorry + end +end diff --git a/test/e.ex b/test/e.ex new file mode 100644 index 0000000..c65e253 --- /dev/null +++ b/test/e.ex @@ -0,0 +1,29 @@ +# +# This file is part of atomvm_packbeam. +# +# Copyright 2025 Winford (Uncle Grumpy) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 OR LGPL-2.1-or-later +# + +defmodule E do + @moduledoc """ + Test module for TestPackbeam + """ + + def b_calls_me do + :ok + end +end diff --git a/test/f.ex b/test/f.ex new file mode 100644 index 0000000..bbfc3c8 --- /dev/null +++ b/test/f.ex @@ -0,0 +1,29 @@ +# +# This file is part of atomvm_packbeam. +# +# Copyright 2025 Winford (Uncle Grumpy) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 OR LGPL-2.1-or-later +# + +defmodule F do + @moduledoc """ + Test module for TestPackbeam + """ + + def c_calls_me do + :ok + end +end diff --git a/test/packbeam_test.exs b/test/packbeam_test.exs new file mode 100644 index 0000000..23d2aff --- /dev/null +++ b/test/packbeam_test.exs @@ -0,0 +1,86 @@ +# +# This file is part of atomvm_packbeam. +# +# Copyright 2025 Winford (Uncle Grumpy) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 OR LGPL-2.1-or-later +# + +defmodule PackbeamTest do + use ExUnit.Case + # doctest PackbeamTest + + test "Pack with prune: false" do + basedir = "_build/" <> Atom.to_string(Mix.env()) <> "/lib/packbeam/ebin" + + files = [ + "Elixir.A.beam", + "Elixir.B.beam", + "Elixir.C.beam", + "Elixir.D.beam", + "Elixir.E.beam", + "Elixir.F.beam" + ] + + filelist = + List.foldr(files, [], fn x, acc -> [String.to_charlist(Path.join(basedir, x)) | acc] end) + + avm = "_build/" <> Atom.to_string(Mix.env()) <> "/testpack.avm" + assert PackBEAM.API.create(avm, filelist, %{prune: false, start_module: A}) == :ok + assert File.exists?(avm) == true + elements0 = PackBEAM.API.list(avm) + [element1, element2, element3, element4, element5, element6 | _elements1] = elements0 + assert List.keyfind(element1, A, 1) == {:module, A} + assert List.keyfind(element2, B, 1) == {:module, B} + assert List.keyfind(element3, C, 1) == {:module, C} + assert List.keyfind(element4, D, 1) == {:module, D} + assert List.keyfind(element5, E, 1) == {:module, E} + assert List.keyfind(element6, F, 1) == {:module, F} + assert List.last(elements0) == element6 + end + + test "Pack with prune: true" do + basedir = "_build/" <> Atom.to_string(Mix.env()) <> "/lib/packbeam/ebin" + + files = [ + "Elixir.A.beam", + "Elixir.B.beam", + "Elixir.C.beam", + "Elixir.D.beam", + "Elixir.E.beam", + "Elixir.F.beam" + ] + + filelist = + List.foldr(files, [], fn x, acc -> [String.to_charlist(Path.join(basedir, x)) | acc] end) + + pruned_avm = "_build/" <> Atom.to_string(Mix.env()) <> "/testpack_pruned.avm" + + assert PackBEAM.API.create(pruned_avm, filelist, %{ + prune: true, + start_module: A + }) == :ok + + assert File.exists?(pruned_avm) == true + elements0 = PackBEAM.API.list(pruned_avm) + [element1, element2, element3, element4, element5 | _elements1] = elements0 + assert List.keyfind(element1, A, 1) == {:module, A} + assert List.keyfind(element2, B, 1) == {:module, B} + assert List.keyfind(element3, C, 1) == {:module, C} + assert List.keyfind(element4, E, 1) == {:module, E} + assert List.keyfind(element5, F, 1) == {:module, F} + assert List.last(elements0) == element5 + end +end diff --git a/test/test_helper.exs b/test/test_helper.exs new file mode 100644 index 0000000..d2f057c --- /dev/null +++ b/test/test_helper.exs @@ -0,0 +1,21 @@ +# +# This file is part of atomvm_packbeam. +# +# Copyright 2025 Winford (Uncle Grumpy) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 OR LGPL-2.1-or-later +# + +ExUnit.start()