├── .circleci └── config.yml ├── .credo.exs ├── .formatter.exs ├── .github ├── CODEOWNERS ├── ISSUE_TEMPLATE │ └── please--open-new-issues-in-membranefranework-membrane_core.md └── workflows │ ├── on_issue_opened.yaml │ └── on_pr_opened.yaml ├── .gitignore ├── LICENSE ├── README.md ├── lib ├── bundlex.ex ├── bundlex │ ├── app.ex │ ├── build_script.ex │ ├── cnode.ex │ ├── cnode │ │ ├── name_store.ex │ │ └── server.ex │ ├── doxygen.ex │ ├── doxygen │ │ └── error.ex │ ├── error.ex │ ├── helper │ │ ├── erlang_helper.ex │ │ ├── git_helper.ex │ │ ├── mix_helper.ex │ │ └── path_helper.ex │ ├── loader.ex │ ├── native.ex │ ├── output.ex │ ├── platform.ex │ ├── platform │ │ ├── freebsd.ex │ │ ├── linux.ex │ │ ├── macosx.ex │ │ ├── nerves.ex │ │ ├── windows32.ex │ │ └── windows64.ex │ ├── port.ex │ ├── project.ex │ ├── project │ │ ├── preprocessor.ex │ │ └── store.ex │ ├── toolchain.ex │ └── toolchain │ │ ├── clang.ex │ │ ├── common │ │ ├── compilers.ex │ │ ├── unix.ex │ │ └── unix │ │ │ └── os_deps.ex │ │ ├── custom.ex │ │ ├── gcc.ex │ │ ├── visual_studio.ex │ │ └── xcode.ex └── mix │ └── tasks │ ├── bundlex.doxygen.ex │ └── compile.bundlex.ex ├── mix.exs ├── mix.lock ├── test ├── bundlex │ └── integration_test.exs └── test_helper.exs └── test_projects ├── example ├── .formatter.exs ├── .gitignore ├── bundlex.exs ├── c_src │ └── example │ │ ├── example_cnode.c │ │ ├── example_port.c │ │ └── foo_nif.c ├── lib │ └── example │ │ └── foo.ex ├── mix.exs ├── pages │ └── doxygen │ │ └── example.md └── test │ ├── example_test.exs │ └── test_helper.exs └── example_lib ├── .formatter.exs ├── .gitignore ├── bundlex.exs ├── c_src └── example_lib │ ├── example_lib_cnode.c │ ├── example_lib_cnode.h │ ├── example_lib_nif.c │ └── example_lib_nif.h └── mix.exs /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2.1 2 | 3 | orbs: 4 | elixir: membraneframework/elixir@1.5.1 5 | 6 | workflows: 7 | version: 2 8 | build: 9 | jobs: 10 | - elixir/build_test: ¶meters 11 | executor: elixir 12 | filters: 13 | tags: 14 | only: /v.*/ 15 | - elixir/test: 16 | <<: *parameters 17 | - elixir/lint: 18 | <<: *parameters 19 | cache-version: 3 20 | - elixir/hex_publish: 21 | requires: 22 | - elixir/build_test 23 | - elixir/test 24 | - elixir/lint 25 | executor: elixir 26 | context: 27 | - Deployment 28 | filters: 29 | branches: 30 | ignore: /.*/ 31 | tags: 32 | only: /v.*/ 33 | cache-version: 3 34 | -------------------------------------------------------------------------------- /.credo.exs: -------------------------------------------------------------------------------- 1 | # This file contains the configuration for Credo and you are probably reading 2 | # this after creating it with `mix credo.gen.config`. 3 | # 4 | # If you find anything wrong or unclear in this file, please report an 5 | # issue on GitHub: https://github.com/rrrene/credo/issues 6 | # 7 | %{ 8 | # 9 | # You can have as many configs as you like in the `configs:` field. 10 | configs: [ 11 | %{ 12 | # 13 | # Run any config using `mix credo -C `. If no config name is given 14 | # "default" is used. 15 | # 16 | name: "default", 17 | # 18 | # These are the files included in the analysis: 19 | files: %{ 20 | # 21 | # You can give explicit globs or simply directories. 22 | # In the latter case `**/*.{ex,exs}` will be used. 23 | # 24 | included: [ 25 | "lib/", 26 | "src/", 27 | "test/", 28 | "web/", 29 | "apps/*/lib/", 30 | "apps/*/src/", 31 | "apps/*/test/", 32 | "apps/*/web/" 33 | ], 34 | excluded: [~r"/_build/", ~r"/deps/", ~r"/node_modules/"] 35 | }, 36 | # 37 | # Load and configure plugins here: 38 | # 39 | plugins: [], 40 | # 41 | # If you create your own checks, you must specify the source files for 42 | # them here, so they can be loaded by Credo before running the analysis. 43 | # 44 | requires: [], 45 | # 46 | # If you want to enforce a style guide and need a more traditional linting 47 | # experience, you can change `strict` to `true` below: 48 | # 49 | strict: false, 50 | # 51 | # To modify the timeout for parsing files, change this value: 52 | # 53 | parse_timeout: 5000, 54 | # 55 | # If you want to use uncolored output by default, you can change `color` 56 | # to `false` below: 57 | # 58 | color: true, 59 | # 60 | # You can customize the parameters of any check by adding a second element 61 | # to the tuple. 62 | # 63 | # To disable a check put `false` as second element: 64 | # 65 | # {Credo.Check.Design.DuplicatedCode, false} 66 | # 67 | checks: [ 68 | # 69 | ## Consistency Checks 70 | # 71 | {Credo.Check.Consistency.ExceptionNames, []}, 72 | {Credo.Check.Consistency.LineEndings, []}, 73 | {Credo.Check.Consistency.ParameterPatternMatching, []}, 74 | {Credo.Check.Consistency.SpaceAroundOperators, []}, 75 | {Credo.Check.Consistency.SpaceInParentheses, []}, 76 | {Credo.Check.Consistency.TabsOrSpaces, []}, 77 | 78 | # 79 | ## Design Checks 80 | # 81 | # You can customize the priority of any check 82 | # Priority values are: `low, normal, high, higher` 83 | # 84 | {Credo.Check.Design.AliasUsage, 85 | [priority: :low, if_nested_deeper_than: 2, if_called_more_often_than: 0]}, 86 | # You can also customize the exit_status of each check. 87 | # If you don't want TODO comments to cause `mix credo` to fail, just 88 | # set this value to 0 (zero). 89 | # 90 | {Credo.Check.Design.TagTODO, [exit_status: 0]}, 91 | {Credo.Check.Design.TagFIXME, []}, 92 | 93 | # 94 | ## Readability Checks 95 | # 96 | {Credo.Check.Readability.AliasOrder, [priority: :normal]}, 97 | {Credo.Check.Readability.FunctionNames, []}, 98 | {Credo.Check.Readability.LargeNumbers, []}, 99 | {Credo.Check.Readability.MaxLineLength, [priority: :low, max_length: 120]}, 100 | {Credo.Check.Readability.ModuleAttributeNames, []}, 101 | {Credo.Check.Readability.ModuleDoc, []}, 102 | {Credo.Check.Readability.ModuleNames, []}, 103 | {Credo.Check.Readability.ParenthesesInCondition, []}, 104 | {Credo.Check.Readability.ParenthesesOnZeroArityDefs, parens: true}, 105 | {Credo.Check.Readability.PredicateFunctionNames, []}, 106 | {Credo.Check.Readability.PreferImplicitTry, []}, 107 | {Credo.Check.Readability.RedundantBlankLines, []}, 108 | {Credo.Check.Readability.Semicolons, []}, 109 | {Credo.Check.Readability.SpaceAfterCommas, []}, 110 | {Credo.Check.Readability.StringSigils, []}, 111 | {Credo.Check.Readability.TrailingBlankLine, []}, 112 | {Credo.Check.Readability.TrailingWhiteSpace, []}, 113 | {Credo.Check.Readability.UnnecessaryAliasExpansion, []}, 114 | {Credo.Check.Readability.VariableNames, []}, 115 | {Credo.Check.Readability.WithSingleClause, false}, 116 | 117 | # 118 | ## Refactoring Opportunities 119 | # 120 | {Credo.Check.Refactor.CondStatements, []}, 121 | {Credo.Check.Refactor.CyclomaticComplexity, []}, 122 | {Credo.Check.Refactor.FunctionArity, []}, 123 | {Credo.Check.Refactor.LongQuoteBlocks, []}, 124 | {Credo.Check.Refactor.MapInto, false}, 125 | {Credo.Check.Refactor.MatchInCondition, []}, 126 | {Credo.Check.Refactor.NegatedConditionsInUnless, []}, 127 | {Credo.Check.Refactor.NegatedConditionsWithElse, []}, 128 | {Credo.Check.Refactor.Nesting, []}, 129 | {Credo.Check.Refactor.UnlessWithElse, []}, 130 | {Credo.Check.Refactor.WithClauses, []}, 131 | 132 | # 133 | ## Warnings 134 | # 135 | {Credo.Check.Warning.BoolOperationOnSameValues, []}, 136 | {Credo.Check.Warning.ExpensiveEmptyEnumCheck, []}, 137 | {Credo.Check.Warning.IExPry, []}, 138 | {Credo.Check.Warning.IoInspect, []}, 139 | {Credo.Check.Warning.LazyLogging, false}, 140 | {Credo.Check.Warning.MixEnv, []}, 141 | {Credo.Check.Warning.OperationOnSameValues, []}, 142 | {Credo.Check.Warning.OperationWithConstantResult, []}, 143 | {Credo.Check.Warning.RaiseInsideRescue, []}, 144 | {Credo.Check.Warning.UnusedEnumOperation, []}, 145 | {Credo.Check.Warning.UnusedFileOperation, []}, 146 | {Credo.Check.Warning.UnusedKeywordOperation, []}, 147 | {Credo.Check.Warning.UnusedListOperation, []}, 148 | {Credo.Check.Warning.UnusedPathOperation, []}, 149 | {Credo.Check.Warning.UnusedRegexOperation, []}, 150 | {Credo.Check.Warning.UnusedStringOperation, []}, 151 | {Credo.Check.Warning.UnusedTupleOperation, []}, 152 | {Credo.Check.Warning.UnsafeExec, []}, 153 | 154 | # 155 | # Checks scheduled for next check update (opt-in for now, just replace `false` with `[]`) 156 | 157 | # 158 | # Controversial and experimental checks (opt-in, just replace `false` with `[]`) 159 | # 160 | {Credo.Check.Readability.StrictModuleLayout, 161 | priority: :normal, order: ~w/shortdoc moduledoc behaviour use import require alias/a}, 162 | {Credo.Check.Consistency.MultiAliasImportRequireUse, false}, 163 | {Credo.Check.Consistency.UnusedVariableNames, force: :meaningful}, 164 | {Credo.Check.Design.DuplicatedCode, false}, 165 | {Credo.Check.Readability.AliasAs, false}, 166 | {Credo.Check.Readability.MultiAlias, false}, 167 | {Credo.Check.Readability.Specs, []}, 168 | {Credo.Check.Readability.SinglePipe, false}, 169 | {Credo.Check.Readability.WithCustomTaggedTuple, false}, 170 | {Credo.Check.Refactor.ABCSize, false}, 171 | {Credo.Check.Refactor.AppendSingleItem, false}, 172 | {Credo.Check.Refactor.DoubleBooleanNegation, false}, 173 | {Credo.Check.Refactor.ModuleDependencies, false}, 174 | {Credo.Check.Refactor.NegatedIsNil, false}, 175 | {Credo.Check.Refactor.PipeChainStart, false}, 176 | {Credo.Check.Refactor.VariableRebinding, false}, 177 | {Credo.Check.Warning.LeakyEnvironment, false}, 178 | {Credo.Check.Warning.MapGetUnsafePass, false}, 179 | {Credo.Check.Warning.UnsafeToAtom, false} 180 | 181 | # 182 | # Custom checks can be created using `mix credo.gen.check`. 183 | # 184 | ] 185 | } 186 | ] 187 | } 188 | -------------------------------------------------------------------------------- /.formatter.exs: -------------------------------------------------------------------------------- 1 | locals_without_parens = [ 2 | defnif: 1, 3 | defnifp: 1 4 | ] 5 | 6 | [ 7 | inputs: [ 8 | "{lib,test,config}/**/*.{ex,exs}", 9 | ".formatter.exs", 10 | "mix.exs" 11 | ], 12 | locals_without_parens: locals_without_parens, 13 | export: [ 14 | locals_without_parens: locals_without_parens 15 | ] 16 | ] 17 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @FelonEkonom 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/please--open-new-issues-in-membranefranework-membrane_core.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Please, open new issues in membraneframework/membrane_core 3 | about: New issues related to this repo should be opened there 4 | title: "[DO NOT OPEN]" 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | Please, do not open this issue here. Open it in the [membrane_core](https://github.com/membraneframework/membrane_core) repository instead. 11 | 12 | Thanks for helping us grow :) 13 | -------------------------------------------------------------------------------- /.github/workflows/on_issue_opened.yaml: -------------------------------------------------------------------------------- 1 | name: 'Close issue when opened' 2 | on: 3 | issues: 4 | types: 5 | - opened 6 | jobs: 7 | close: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: Checkout membrane_core 11 | uses: actions/checkout@v3 12 | with: 13 | repository: membraneframework/membrane_core 14 | - name: Close issue 15 | uses: ./.github/actions/close_issue 16 | with: 17 | GITHUB_TOKEN: ${{ secrets.MEMBRANEFRAMEWORKADMIN_TOKEN }} 18 | ISSUE_URL: ${{ github.event.issue.html_url }} 19 | ISSUE_NUMBER: ${{ github.event.issue.number }} 20 | REPOSITORY: ${{ github.repository }} 21 | -------------------------------------------------------------------------------- /.github/workflows/on_pr_opened.yaml: -------------------------------------------------------------------------------- 1 | name: Add PR to Smackore project board, if the author is from outside Membrane Team 2 | on: 3 | pull_request_target: 4 | types: 5 | - opened 6 | jobs: 7 | maybe_add_to_project_board: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: Checkout membrane_core 11 | uses: actions/checkout@v3 12 | with: 13 | repository: membraneframework/membrane_core 14 | - name: Puts PR in "New PRs by community" column in the Smackore project, if the author is from outside Membrane Team 15 | uses: ./.github/actions/add_pr_to_smackore_board 16 | with: 17 | GITHUB_TOKEN: ${{ secrets.MEMBRANEFRAMEWORKADMIN_TOKEN }} 18 | AUTHOR_LOGIN: ${{ github.event.pull_request.user.login }} 19 | PR_URL: ${{ github.event.pull_request.html_url }} 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.gitignore.io/api/c,vim,linux,macos,elixir,windows,visualstudiocode 2 | 3 | ### C ### 4 | # Prerequisites 5 | *.d 6 | 7 | # Object files 8 | *.o 9 | *.ko 10 | *.obj 11 | *.elf 12 | 13 | # Linker output 14 | *.ilk 15 | *.map 16 | *.exp 17 | 18 | # Precompiled Headers 19 | *.gch 20 | *.pch 21 | 22 | # Libraries 23 | *.lib 24 | *.a 25 | *.la 26 | *.lo 27 | 28 | # Shared objects (inc. Windows DLLs) 29 | *.dll 30 | *.so 31 | *.so.* 32 | *.dylib 33 | 34 | # Executables 35 | *.exe 36 | *.out 37 | *.app 38 | *.i*86 39 | *.x86_64 40 | *.hex 41 | 42 | # Debug files 43 | *.dSYM/ 44 | *.su 45 | *.idb 46 | *.pdb 47 | 48 | # Kernel Module Compile Results 49 | *.mod* 50 | *.cmd 51 | .tmp_versions/ 52 | modules.order 53 | Module.symvers 54 | Mkfile.old 55 | dkms.conf 56 | 57 | ### Elixir ### 58 | /_build 59 | /cover 60 | /deps 61 | /doc 62 | /.fetch 63 | erl_crash.dump 64 | *.ez 65 | *.beam 66 | 67 | ### Elixir Patch ### 68 | ### Linux ### 69 | *~ 70 | 71 | # temporary files which can be created if a process still has a handle open of a deleted file 72 | .fuse_hidden* 73 | 74 | # KDE directory preferences 75 | .directory 76 | 77 | # Linux trash folder which might appear on any partition or disk 78 | .Trash-* 79 | 80 | # .nfs files are created when an open file is removed but is still being accessed 81 | .nfs* 82 | 83 | ### macOS ### 84 | *.DS_Store 85 | .AppleDouble 86 | .LSOverride 87 | 88 | # Icon must end with two \r 89 | Icon 90 | 91 | # Thumbnails 92 | ._* 93 | 94 | # Files that might appear in the root of a volume 95 | .DocumentRevisions-V100 96 | .fseventsd 97 | .Spotlight-V100 98 | .TemporaryItems 99 | .Trashes 100 | .VolumeIcon.icns 101 | .com.apple.timemachine.donotpresent 102 | 103 | # Directories potentially created on remote AFP share 104 | .AppleDB 105 | .AppleDesktop 106 | Network Trash Folder 107 | Temporary Items 108 | .apdisk 109 | 110 | ### Vim ### 111 | # swap 112 | .sw[a-p] 113 | .*.sw[a-p] 114 | # session 115 | Session.vim 116 | # temporary 117 | .netrwhist 118 | # auto-generated tag files 119 | tags 120 | 121 | ### VisualStudioCode ### 122 | .vscode/* 123 | !.vscode/settings.json 124 | !.vscode/tasks.json 125 | !.vscode/launch.json 126 | !.vscode/extensions.json 127 | .history 128 | 129 | ### Windows ### 130 | # Windows thumbnail cache files 131 | Thumbs.db 132 | ehthumbs.db 133 | ehthumbs_vista.db 134 | 135 | # Folder config file 136 | Desktop.ini 137 | 138 | # Recycle Bin used on file shares 139 | $RECYCLE.BIN/ 140 | 141 | # Windows Installer files 142 | *.cab 143 | *.msi 144 | *.msm 145 | *.msp 146 | 147 | # Windows shortcuts 148 | *.lnk 149 | 150 | 151 | # End of https://www.gitignore.io/api/c,vim,linux,macos,elixir,windows,visualstudiocode 152 | 153 | /tmp 154 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2018 Software Mansion 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Bundlex 2 | 3 | [![Hex.pm](https://img.shields.io/hexpm/v/bundlex.svg)](https://hex.pm/packages/bundlex) 4 | [![API Docs](https://img.shields.io/badge/api-docs-yellow.svg?style=flat)](https://hexdocs.pm/bundlex/) 5 | [![CircleCI](https://circleci.com/gh/membraneframework/bundlex.svg?style=svg)](https://circleci.com/gh/membraneframework/bundlex) 6 | 7 | Bundlex is a multi-platform tool for compiling C and C++ code along with elixir projects, for use in NIFs, CNodes and Ports. The tool also provides a convenient way of accessing compiled code in elixir modules. 8 | 9 | Bundlex has been tested on Linux, Mac OS and FreeBSD. There's some support for Windows as well, but it's experimental and unstable (see issues for details). 10 | 11 | Bundlex also supports cross-compilation and has been tested with platforms running Nerves. 12 | 13 | This tool is maintained by the [Membrane Framework](https://membraneframework.org/) team. 14 | 15 | ## Installation 16 | 17 | To install, you need to configure Mix project as follows: 18 | 19 | ```elixir 20 | defmodule MyApp.Mixfile do 21 | use Mix.Project 22 | 23 | def project do 24 | [ 25 | app: :my_app, 26 | compilers: [:bundlex] ++ Mix.compilers, # add bundlex to compilers 27 | deps: deps(), 28 | # ... 29 | ] 30 | end 31 | 32 | defp deps() do 33 | [ 34 | {:bundlex, "~> 1.5"} 35 | ] 36 | end 37 | end 38 | ``` 39 | 40 | and create `bundlex.exs` file in the project root folder, containing Bundlex project module: 41 | 42 | ```elixir 43 | defmodule MyApp.BundlexProject do 44 | use Bundlex.Project 45 | 46 | def project() do 47 | [] 48 | end 49 | end 50 | ``` 51 | 52 | Now your project does not contain any C sources, but should compile successfully, and some Bundlex messages should be printed while compilation proceeds. 53 | 54 | ## Usage 55 | 56 | ### Adding natives to project 57 | 58 | Adding natives can be done in `project/0` function of Bundlex project module in the following way: 59 | 60 | ```elixir 61 | defmodule MyApp.BundlexProject do 62 | use Bundlex.Project 63 | 64 | def project() do 65 | [ 66 | natives: natives(Bundlex.platform), 67 | libs: libs() 68 | ] 69 | end 70 | 71 | defp natives(:linux) do 72 | [ 73 | my_native: [ 74 | sources: ["something.c", "linux_specific.c"], 75 | interface: :nif 76 | ], 77 | my_other_native: [ 78 | sources: ["something_other.c", "linux_specific.c"], 79 | interface: :cnode 80 | ], 81 | my_other_native: [ 82 | sources: ["something_more_other.c", "linux_specific.c"], 83 | interface: :port 84 | ] 85 | ] 86 | end 87 | 88 | defp natives(_platform) do 89 | [ 90 | my_native: [ 91 | sources: ["something.c", "multiplatform.c"], 92 | interface: :nif 93 | ], 94 | my_other_native: [ 95 | sources: ["something_other.c", "multiplatform.c"], 96 | interface: :cnode 97 | ], 98 | my_other_native: [ 99 | sources: ["something_more_other.c", "multiplatform.c"], 100 | interface: :port 101 | ] 102 | ] 103 | end 104 | 105 | defp libs() do 106 | [ 107 | my_lib: [ 108 | sources: ["something.c"], 109 | interface: :nif 110 | ], 111 | my_lib: [ 112 | sources: ["something_other.c"], 113 | interface: :cnode 114 | ] 115 | ] 116 | end 117 | end 118 | ``` 119 | 120 | As we can see, we can specify two types of resources: 121 | - natives - code implemented in C that will be used within Elixir code 122 | - libs - can be used by natives or other libs as [dependencies](#Dependencies) 123 | 124 | By default, the sources should reside in `project_root/c_src/my_app` directory. 125 | 126 | For more details and available options, see [Bundlex.Project.native_config](https://hexdocs.pm/bundlex/Bundlex.Project.html#t:native_config/0). 127 | 128 | ### Dependencies 129 | 130 | Each native can have dependencies - libs that are statically linked to it and can be included in its native code like `#include lib_name/some_header.h`. The following rules apply: 131 | - To add dependencies from a separate project, it must be available via Mix. 132 | - Only libs can be added as dependencies. 133 | - Each dependency of a native must specify the same or no interface. If there exist multiple versions of dependency with different interfaces, the proper version is selected automatically. 134 | - A lib that specifies no interface can depend on libs with no interfaces only. 135 | 136 | ### Compilation options 137 | 138 | The following command-line arguments can be passed: 139 | - `--store-scripts` - if set, shell scripts are stored in the project 140 | root folder for further analysis. 141 | 142 | ### Loading NIFs in modules 143 | 144 | NIFs compiled with Bundlex can be loaded the same way as any other NIFs (see [`:erlang.load_nif/2`](http://erlang.org/doc/man/erlang.html#load_nif-2)), but Bundlex provides `Bundlex.Loader` module to save you some boilerplate: 145 | 146 | ```elixir 147 | defmodule MyApp.SomeNativeStuff do 148 | use Bundlex.Loader, nif: :my_nif 149 | 150 | def normal_function(a, b, c, d) do 151 | private_native_function(a+b, c+d) 152 | end 153 | 154 | defnif native_function(a, b) 155 | 156 | defnifp private_native_function(x, y) 157 | 158 | end 159 | ``` 160 | 161 | Note that unlike when using `:erlang.load_nif/2`, here `def`s and `defp`s can be used to create usual functions, native ones are declared with `defnif` and `defnifp`. This is achieved by creating a new module under the hood, and that is why the module passed to C macro `ERL_NIF_INIT` has to be succeeded by `.Nif`, i.e. 162 | ```C 163 | ERL_NIF_INIT(MyApp.SomeNativeStuff.Nif, funs, load, NULL, upgrade, unload) 164 | ``` 165 | 166 | Despite this, any native erlang macros and functions shall be used as usual, as described at http://erlang.org/doc/man/erl_nif.html 167 | 168 | ### Interacting with CNodes 169 | 170 | As in the case of NIFs, CNodes compiled with Bundlex can be used like any other CNodes (see built-in `Node` module), while some useful stuff for interacting with them is provided. `Bundlex.CNode` module contains utilities that make it easier to spawn and control CNodes, and allow them to treat them more like usual Elixir processes. Check out the documentation for more details. 171 | 172 | ### Interacting with Ports 173 | 174 | Similarly to CNodes Bundlex provides `Bundlex.Port` module for a little easier interacting with Ports. 175 | Please refer to the module's documentation to see how to use it. 176 | 177 | ### Cross-compilation 178 | 179 | With proper setup, Bundlex can support cross-compilation. When using Nerves it should work out of the box. 180 | 181 | Not relying on Nerves and using your own toolchain is also possible, although it wasn't tested. In this scenario, the following environment variables need to be set during compilation (when changing the target bundlex also needs to be recompiled): 182 | - `CROSSCOMPILE` - value is not important, just needs to be set 183 | - `CC` - path to the C compiler for cross-compiling to the target 184 | - `CFLAGS` - C compilation flags 185 | - `CXX` - path to the C++ compiler for cross-compiling to the target 186 | - `CXXFLAGS` - C++ compilation flags 187 | - `LDFLAGS` - Linker flags 188 | 189 | If you wish for `Bundlex.get_target/0` to return accurate information about your target, set the following environment variables: 190 | - `TARGET_ARCH` - The target CPU architecture (e.g., `arm`, `aarch64`, `mipsel`, `x86_64`, `riscv64`) 191 | - `TARGET_VENDOR` - Vendor of your target platform 192 | - `TARGET_OS` - The targes OS (e.g. `linux`) 193 | - `TARGET_ABI` - The target ABI (e.g., `gnueabihf`, `musl`) 194 | 195 | When cross-compiling some warnings may be raised about not being able to load nifs, but that's expected, since they are most likely built for different architecture. 196 | 197 | ### Documentation of the native code 198 | 199 | Bundlex provides a way to generate documentation of the native code. The documentation is generated using [Doxygen](http://www.doxygen.nl/). 200 | 201 | To do so, run `$ mix bundlex.doxygen` command. The documentation is generated for each native separately. The documentation of the native `project_name` will be generated in `doc/bundlex/project_name` directory. Additionally, hex doc page with the link to the Doxygen documentation is generated in the `pages/doxygen/project_name.md` and should be included in the `mix.exs` file: 202 | 203 | ```elixir 204 | defp docs do 205 | [ 206 | extras: [ 207 | "pages/doxygen/project_name.md", 208 | ... 209 | ], 210 | ... 211 | ] 212 | end 213 | ``` 214 | If you want to keep own changes in the `pages/doxygen/project_name.md` file, you can use `--no` option to skip the generation of this file. Otherwise, if you want the file to be always overwritten, use `--yes` option. 215 | 216 | After that, the documentation can be generated with `mix docs` command. 217 | 218 | ### Include native documentation in the hex docs 219 | 220 | To include the native documentation in the hex docs, you need to generate the documentation with `$ mix bundlex.doxygen` command and include hex page in the `extras`, before running `$ mix hex.publish` command. 221 | 222 | ## More examples 223 | 224 | More advanced examples can be found in our [test_projects](https://github.com/membraneframework/bundlex/tree/master/test_projects) 225 | or in our [repositories](https://github.com/membraneframework) where we use Bundlex e.g. in [Unifex](https://github.com/membraneframework/unifex). 226 | 227 | ## Copyright and License 228 | 229 | Copyright 2018, [Software Mansion](https://swmansion.com/?utm_source=git&utm_medium=readme&utm_campaign=membrane) 230 | 231 | [![Software Mansion](https://logo.swmansion.com/logo?color=white&variant=desktop&width=200&tag=membrane-github)](https://swmansion.com/?utm_source=git&utm_medium=readme&utm_campaign=membrane) 232 | 233 | Licensed under the [Apache License, Version 2.0](LICENSE) 234 | -------------------------------------------------------------------------------- /lib/bundlex.ex: -------------------------------------------------------------------------------- 1 | defmodule Bundlex do 2 | @moduledoc """ 3 | Some common utility functions. 4 | """ 5 | 6 | alias Bundlex.Helper.MixHelper 7 | alias Bundlex.Platform 8 | 9 | @type platform_t :: :linux | :macosx | :windows32 | :windows64 | :nerves | :custom 10 | 11 | @typedoc """ 12 | A map containing four fields that describe the target platform. 13 | 14 | It consists of: 15 | * architecture - e.g. `x86_64` or `arm64` 16 | * vendor - e.g. `pc` 17 | * os - operating system, e.g. `linux` or `darwin20.6.0` 18 | * abi - application binary interface, e.g. `musl` or `gnu` 19 | """ 20 | @type target :: 21 | %{ 22 | architecture: String.t(), 23 | vendor: String.t(), 24 | os: String.t(), 25 | abi: String.t() 26 | } 27 | 28 | @doc """ 29 | A function returning information about the target platform. In case of cross-compilation the 30 | information can be provided by setting appropriate environment variables. 31 | """ 32 | @spec get_target() :: target() 33 | case System.fetch_env("CROSSCOMPILE") do 34 | :error -> 35 | if Platform.get_target!() in [:windows32, :windows64] do 36 | def get_target() do 37 | %{ 38 | architecture: System.fetch_env!("PROCESSOR_ARCHITECTURE"), 39 | vendor: "pc", 40 | os: "windows", 41 | abi: "unknown" 42 | } 43 | end 44 | else 45 | def get_target() do 46 | [architecture, vendor, os | maybe_abi] = 47 | :erlang.system_info(:system_architecture) |> List.to_string() |> String.split("-") 48 | 49 | %{ 50 | architecture: architecture, 51 | vendor: vendor, 52 | os: os, 53 | abi: List.first(maybe_abi) || "unknown" 54 | } 55 | end 56 | end 57 | 58 | {:ok, _crosscompile} -> 59 | target = 60 | [ 61 | architecture: "TARGET_ARCH", 62 | vendor: "TARGET_VENDOR", 63 | os: "TARGET_OS", 64 | abi: "TARGET_ABI" 65 | ] 66 | |> Map.new(fn {key, env} -> {key, System.get_env(env, "unknown")} end) 67 | |> Macro.escape() 68 | 69 | def get_target() do 70 | unquote(target) 71 | end 72 | end 73 | 74 | @doc """ 75 | Returns current platform name. 76 | """ 77 | @deprecated "Use Bundlex.get_target/0 instead" 78 | @dialyzer {:nowarn_function, platform: 0} 79 | @spec platform() :: platform_t() 80 | def platform() do 81 | case Platform.get_target!() do 82 | :custom -> :nerves 83 | platform -> platform 84 | end 85 | end 86 | 87 | @doc """ 88 | Returns family of the platform obtained with `platform/0`. 89 | """ 90 | @spec family() :: :unix | :windows | :custom 91 | def family() do 92 | Platform.family(Platform.get_target!()) 93 | end 94 | 95 | @doc """ 96 | Returns path where compiled native is stored. 97 | """ 98 | @spec build_path(application :: atom, native :: atom, native_interface :: atom) :: String.t() 99 | def build_path(application \\ MixHelper.get_app!(), native, native_interface) do 100 | Bundlex.Toolchain.output_path(application, native, native_interface) 101 | end 102 | end 103 | -------------------------------------------------------------------------------- /lib/bundlex/app.ex: -------------------------------------------------------------------------------- 1 | defmodule Bundlex.App do 2 | @moduledoc false 3 | use Application 4 | 5 | @impl true 6 | def start(_type, _args) do 7 | children = [Bundlex.CNode.NameStore, Bundlex.Project.Store] 8 | opts = [strategy: :one_for_one, name: __MODULE__] 9 | Supervisor.start_link(children, opts) 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /lib/bundlex/build_script.ex: -------------------------------------------------------------------------------- 1 | defmodule Bundlex.BuildScript do 2 | @moduledoc false 3 | 4 | use Bunch 5 | 6 | alias Bundlex.{Output, Platform} 7 | 8 | @script_ext unix: ".sh", windows: ".bat" 9 | @script_prefix unix: "#!/bin/sh\n", windows: "@echo off\r\n" 10 | 11 | @type t :: %__MODULE__{ 12 | commands: [command_t] 13 | } 14 | 15 | @type command_t :: String.t() 16 | 17 | defstruct commands: [] 18 | 19 | @doc """ 20 | Creates new build script. 21 | """ 22 | @spec new([command_t]) :: t 23 | def new(commands \\ []) do 24 | %__MODULE__{commands: commands} 25 | end 26 | 27 | @spec run(t, Platform.name_t()) :: 28 | :ok | {:error, {:run_build_script, return_code: integer, command: String.t()}} 29 | def run(script, platform) 30 | 31 | def run(%__MODULE__{commands: []}, _platform) do 32 | Output.warn("The list of commands is empty, did you forget to specify natives?") 33 | end 34 | 35 | def run(%__MODULE__{commands: commands}, platform) do 36 | family = Platform.family(platform) 37 | cmd = commands |> join_commands(family) 38 | 39 | case cmd |> Mix.shell().cmd() do 40 | 0 -> :ok 41 | ret -> {:error, {:run_build_script, return_code: ret, command: cmd}} 42 | end 43 | end 44 | 45 | @spec store(t, Platform.name_t(), String.t()) :: {:ok, {String.t(), String.t()}} 46 | def store(%__MODULE__{commands: commands}, platform, name \\ "bundlex") do 47 | family = Platform.family(platform) 48 | script_name = name <> @script_ext[family] 49 | script_prefix = @script_prefix[family] 50 | script = script_prefix <> (commands |> join_commands(family)) 51 | 52 | with :ok <- File.write(script_name, script), 53 | :ok <- if(family == :unix, do: File.chmod!(script_name, 0o755), else: :ok) do 54 | {:ok, {script_name, script}} 55 | end 56 | end 57 | 58 | defp join_commands(commands, :windows) do 59 | Enum.join(commands, " && ") <> "\r\n" 60 | end 61 | 62 | defp join_commands(commands, _other) do 63 | Enum.map_join(commands, " && \\\n", &"(#{&1})") <> "\n" 64 | end 65 | end 66 | -------------------------------------------------------------------------------- /lib/bundlex/cnode.ex: -------------------------------------------------------------------------------- 1 | defmodule Bundlex.CNode do 2 | @moduledoc """ 3 | Utilities to ease interaction with Bundlex-based CNodes, so they can be treated 4 | more like Elixir processes / `GenServer`s. 5 | """ 6 | 7 | use Bunch 8 | alias Bundlex.Helper.MixHelper 9 | 10 | @enforce_keys [:server, :node] 11 | defstruct @enforce_keys 12 | 13 | @typedoc """ 14 | Reference to the CNode. 15 | 16 | Consists of pid of CNode's associated server and CNode name. 17 | """ 18 | @type t :: %__MODULE__{ 19 | server: pid, 20 | node: node 21 | } 22 | 23 | @type on_start_t :: {:ok, t} | {:error, :spawn_cnode | :connect_to_cnode} 24 | 25 | @doc """ 26 | Spawns and connects to CNode `cnode_name` from application of calling module. 27 | 28 | See `#{inspect(__MODULE__)}.start_link/2` for more details. 29 | """ 30 | defmacro start_link(native_name) do 31 | app = MixHelper.get_app!(__CALLER__.module) 32 | 33 | quote do 34 | unquote(__MODULE__).start_link(unquote(app), unquote(native_name)) 35 | end 36 | end 37 | 38 | @doc """ 39 | Spawns and connects to CNode `cnode_name` from application of calling module. 40 | 41 | See `#{inspect(__MODULE__)}.start/2` for more details. 42 | """ 43 | defmacro start(native_name) do 44 | app = MixHelper.get_app!(__CALLER__.module) 45 | 46 | quote do 47 | unquote(__MODULE__).start(unquote(app), unquote(native_name)) 48 | end 49 | end 50 | 51 | @doc """ 52 | Spawns and connects to CNode `cnode_name` from application `app`. 53 | 54 | The CNode is passed the following command line arguments: 55 | - host name, 56 | - alive name, 57 | - node name, 58 | - creation number. 59 | 60 | After CNode startup, these parameters should be passed to 61 | [`ei_connect_xinit`](http://erlang.org/doc/man/ei_connect.html#ei_connect_xinit) 62 | function, and CNode should be published and await connection. Once the CNode is 63 | published, it should print a line starting with `ready` to the standard output 64 | **and flush the standard output** to avoid the line being buffered. 65 | 66 | Under the hood, this function starts an associated server, which is responsible 67 | for monitoring the CNode and monitoring calling process to be able to do proper 68 | cleanup upon a crash. On startup, the server does the following: 69 | 1. Makes current node distributed if it is not done yet (see `Node.start/3`). 70 | 1. Assigns CNode a unique name. 71 | 1. Starts CNode OS process using `Port.open/2`. 72 | 1. Waits (at most 5 seconds) until a line `ready` is printed out 73 | (this line is captured and not forwarded to the stdout). 74 | 1. Connects to the CNode. 75 | 76 | The erlang cookie is passed using the BUNDLEX_ERLANG_COOKIE an environment variable. 77 | """ 78 | @spec start_link(app :: atom, native_name :: atom) :: on_start_t 79 | def start_link(app, native_name) do 80 | do_start(app, native_name, true) 81 | end 82 | 83 | @doc """ 84 | Works the same way as `start_link/2`, but does not link to CNode's associated 85 | server. 86 | """ 87 | @spec start(app :: atom, native_name :: atom) :: on_start_t 88 | def start(app, native_name) do 89 | do_start(app, native_name, false) 90 | end 91 | 92 | defp do_start(app, native_name, link?) do 93 | {:ok, pid} = 94 | GenServer.start( 95 | __MODULE__.Server, 96 | %{app: app, native_name: native_name, caller: self(), link?: link?} 97 | ) 98 | 99 | receive do 100 | {^pid, res} -> res 101 | end 102 | end 103 | 104 | @doc """ 105 | Disconnects from CNode. 106 | 107 | It is the responsibility of the CNode to exit upon connection loss. 108 | """ 109 | @spec stop(t) :: :ok | {:error, :disconnect_cnode} 110 | def stop(%__MODULE__{server: server}) do 111 | GenServer.call(server, :stop) 112 | end 113 | 114 | @doc """ 115 | Starts monitoring CNode from the calling process. 116 | """ 117 | @spec monitor(t) :: reference 118 | def monitor(%__MODULE__{server: server}) do 119 | Process.monitor(server) 120 | end 121 | 122 | @doc """ 123 | Makes a synchronous call to CNode and waits for its reply. 124 | 125 | The CNode is supposed to send back a `{cnode, response}` tuple where `cnode` 126 | is the node name of CNode. If the response doesn't come in within `timeout`, 127 | error is raised. 128 | 129 | Messages are exchanged directly (without interacting with CNode's associated 130 | server). 131 | """ 132 | @spec call(t, message :: term, timeout :: non_neg_integer | :infinity) :: response :: term 133 | def call(%__MODULE__{node: node}, message, timeout \\ 5000) do 134 | Kernel.send({:any, node}, message) 135 | 136 | receive do 137 | {^node, response} -> response 138 | after 139 | timeout -> raise "Timeout upon call to the CNode #{inspect(node)}" 140 | end 141 | end 142 | 143 | @doc """ 144 | Sends a message to cnode. 145 | 146 | The message is exchanged directly (without interacting with CNode's associated 147 | server). 148 | """ 149 | @spec send(t, message :: term) :: :ok 150 | def send(%__MODULE__{node: node}, message) do 151 | Kernel.send({:any, node}, message) 152 | :ok 153 | end 154 | end 155 | -------------------------------------------------------------------------------- /lib/bundlex/cnode/name_store.ex: -------------------------------------------------------------------------------- 1 | defmodule Bundlex.CNode.NameStore do 2 | @moduledoc false 3 | # Responsibility of this module is to provide unique names for CNodes. 4 | # Name of each CNode consists of prefix, sequential number (referred to as 5 | # `seq_num`) and unique current application identifier (referred to as `self_id`). 6 | # In order not to create too many atoms, which are not garbage-collected, 7 | # names of dead CNodes are reused (they should be returned via `return_name/2`), 8 | # and therefore `creation` numbers are returned along with names to distinguish 9 | # between old and new CNode instances. 10 | 11 | use Agent 12 | 13 | @spec start_link(GenServer.options()) :: Agent.on_start() 14 | def start_link(opts \\ []) do 15 | Agent.start_link( 16 | fn -> %{seq_num: 0, q: Qex.new(), creations: %{}, self_id: UUID.uuid4()} end, 17 | opts ++ [name: __MODULE__] 18 | ) 19 | end 20 | 21 | @spec get_self_name() :: name :: atom 22 | def get_self_name() do 23 | Agent.get(__MODULE__, fn %{self_id: self_id} -> :"bundlex_app_#{self_id}" end) 24 | end 25 | 26 | @spec get_name() :: {name :: atom, creation :: non_neg_integer} 27 | def get_name() do 28 | Agent.get_and_update(__MODULE__, fn state -> 29 | {name, q, seq_num} = 30 | case state.q |> Qex.pop() do 31 | {{:value, v}, q} -> 32 | {v, q, state.seq_num} 33 | 34 | {:empty, q} -> 35 | {:"bundlex_cnode_#{state.seq_num}_#{state.self_id}", q, state.seq_num + 1} 36 | end 37 | 38 | {{name, state.creations |> Map.get(name, 0)}, %{state | seq_num: seq_num, q: q}} 39 | end) 40 | end 41 | 42 | @spec return_name(name :: atom) :: :ok 43 | def return_name(name) do 44 | Agent.update(__MODULE__, fn state -> 45 | state 46 | |> Map.update!(:q, &Qex.push(&1, name)) 47 | |> update_in([:creations, name], &((&1 || 0) + 1)) 48 | end) 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /lib/bundlex/cnode/server.ex: -------------------------------------------------------------------------------- 1 | defmodule Bundlex.CNode.Server do 2 | @moduledoc false 3 | 4 | use GenServer 5 | require Logger 6 | alias Bundlex.CNode.NameStore 7 | 8 | @impl true 9 | def init(opts) do 10 | Process.flag(:trap_exit, true) 11 | if opts.link?, do: Process.monitor(opts.caller) 12 | :ok = ensure_node_distributed() 13 | {name, creation} = NameStore.get_name() 14 | cnode = :"#{name}@#{host_name()}" 15 | cookie = Node.get_cookie() |> Atom.to_charlist() 16 | 17 | port = 18 | Port.open( 19 | {:spawn_executable, Bundlex.build_path(opts.app, opts.native_name, :cnode)}, 20 | args: [host_name(), name, cnode, "#{creation}"], 21 | line: 2048, 22 | env: [{~c"BUNDLEX_ERLANG_COOKIE", cookie}] 23 | ) 24 | 25 | Process.send_after(self(), :timeout, 5000) 26 | 27 | {:ok, 28 | %{ 29 | port: port, 30 | status: :waiting, 31 | caller: opts.caller, 32 | link?: opts.link?, 33 | cnode: cnode, 34 | msg_part?: false 35 | }} 36 | catch 37 | err, reason -> {:stop, {err, reason}} 38 | end 39 | 40 | @impl true 41 | def handle_info( 42 | {port, {:data, {:eol, ~c"ready"}}}, 43 | %{port: port, status: :waiting, msg_part?: false} = state 44 | ) do 45 | case Node.connect(state.cnode) do 46 | true -> 47 | send(state.caller, {self(), {:ok, %Bundlex.CNode{server: self(), node: state.cnode}}}) 48 | {:noreply, %{state | status: :connected}} 49 | 50 | _connect_failed -> 51 | send(state.caller, {self(), {:error, :connect_to_cnode}}) 52 | {:stop, :normal, state} 53 | end 54 | end 55 | 56 | def handle_info({port, {:data, {flag, data}}}, %{port: port} = state) do 57 | Logger.info("cnode#{inspect(self())}: #{data}") 58 | {:noreply, %{state | msg_part?: flag == :noeol}} 59 | end 60 | 61 | def handle_info(:timeout, state) do 62 | case state.status do 63 | :waiting -> 64 | send(state.caller, {self(), {:error, :spawn_cnode}}) 65 | {:stop, :normal, state} 66 | 67 | _status -> 68 | {:noreply, state} 69 | end 70 | end 71 | 72 | def handle_info({:DOWN, _ref, :process, pid, _reason}, %{caller: pid} = state) do 73 | disconnect(state.cnode) 74 | {:stop, :normal, state} 75 | end 76 | 77 | def handle_info({:EXIT, port, :normal}, %{port: port} = state) do 78 | {:stop, :normal, state} 79 | end 80 | 81 | def handle_info({:EXIT, port, reason}, %{port: port} = state) do 82 | if state.link?, do: Process.exit(state.caller, :shutdown) 83 | {:stop, reason, state} 84 | end 85 | 86 | def handle_info({:EXIT, _from, :normal}, state) do 87 | {:noreply, state} 88 | end 89 | 90 | def handle_info({:EXIT, _from, reason}, state) do 91 | if state.link?, do: Process.exit(state.caller, reason) 92 | {:stop, reason, state} 93 | end 94 | 95 | @impl true 96 | def handle_call(:stop, _from, state) do 97 | {:stop, :normal, disconnect(state.cnode), state} 98 | end 99 | 100 | defp ensure_node_distributed(empd_status \\ :unknown) do 101 | if Node.alive?() do 102 | :ok 103 | else 104 | case Node.start(NameStore.get_self_name(), :shortnames) do 105 | {:ok, _pid} -> 106 | Node.set_cookie(:bundlex_cookie) 107 | :ok 108 | 109 | {:error, {:already_started, _pid}} -> 110 | # In case the node has been started after the `Node.alive?` check 111 | :ok 112 | 113 | {:error, _reason} when empd_status == :unknown -> 114 | Logger.info("Trying to start epmd...") 115 | System.cmd("epmd", ~w(-daemon)) 116 | # ensure epmd finished starting 117 | System.cmd("epmd", ~w(-names)) 118 | ensure_node_distributed(:start_tried) 119 | 120 | {:error, reason} -> 121 | {:error, reason} 122 | end 123 | end 124 | end 125 | 126 | defp disconnect(cnode) do 127 | case Node.disconnect(cnode) do 128 | true -> 129 | NameStore.return_name(cnode |> node_name) 130 | :ok 131 | 132 | _disconnect_failed -> 133 | {:error, :disconnect_cnode} 134 | end 135 | end 136 | 137 | defp node_name(node) do 138 | node |> to_string() |> String.split("@") |> List.first() 139 | end 140 | 141 | defp host_name(node \\ Node.self()) do 142 | node |> to_string() |> String.split("@") |> List.last() 143 | end 144 | end 145 | -------------------------------------------------------------------------------- /lib/bundlex/doxygen.ex: -------------------------------------------------------------------------------- 1 | defmodule Bundlex.Doxygen do 2 | @moduledoc """ 3 | Module responsible for generating doxygen documentation for Bundlex projects. 4 | """ 5 | 6 | alias Bundlex.Project 7 | 8 | @type doxygen_t :: %{ 9 | project_name: String.t(), 10 | doxyfile_path: String.t(), 11 | doxygen_path: String.t(), 12 | page_path: String.t() 13 | } 14 | 15 | @doc """ 16 | Prepares struct with all necessary filepaths for the native documentation 17 | """ 18 | @spec doxygen(Project.t()) :: doxygen_t() 19 | def doxygen(project) do 20 | project_name = Atom.to_string(project.app) 21 | 22 | %{ 23 | project_name: project_name, 24 | doxyfile_path: doxyfile_path(project), 25 | doxygen_path: doxygen_path(project), 26 | page_path: page_path(project) 27 | } 28 | end 29 | 30 | defp doxyfile_path(project) do 31 | project_name = "#{project.app}" 32 | project_dirpath = Path.join(["doc", "doxygen", project_name]) 33 | Path.join(project_dirpath, "Doxyfile") 34 | end 35 | 36 | defp doxygen_path(project) do 37 | project_name = "#{project.app}" 38 | Path.join(["doc", "doxygen", project_name]) 39 | end 40 | 41 | defp page_path(project) do 42 | Path.join(["pages", "doxygen", "#{project.app}.md"]) 43 | end 44 | 45 | @doc """ 46 | Generates doxyfile in the c_src/project directory for Bundlex project. 47 | """ 48 | @spec generate_doxyfile(doxygen_t()) :: :ok 49 | def generate_doxyfile(doxygen) do 50 | create_doxyfile_template(doxygen) 51 | 52 | keywords_to_change = keywords_to_change(doxygen) 53 | update_doxyfile_keywords(doxygen, keywords_to_change) 54 | end 55 | 56 | defp create_doxyfile_template(doxygen) do 57 | ensure_doxygen_dir_existence(doxygen) 58 | cmd_bundlex(["-g", doxygen.doxyfile_path]) 59 | end 60 | 61 | defp cmd_bundlex(args) do 62 | with {_res, 0} <- 63 | System.cmd("doxygen", args) do 64 | :ok 65 | else 66 | {output, status} -> 67 | error_message = 68 | "Running doxygen command with args: #{inspect(args)} failed with exit code: #{inspect(status)} and output: #{inspect(output)}" 69 | 70 | raise error_message 71 | end 72 | rescue 73 | e in ErlangError -> 74 | case e do 75 | %ErlangError{original: :enoent} -> 76 | reraise Bundlex.Doxygen.Error, 77 | [ 78 | message: 79 | "Failed to run `doxygen` command with args #{inspect(args)}. Ensure, that you have `doxygen` available on your machine" 80 | ], 81 | __STACKTRACE__ 82 | 83 | e -> 84 | reraise e, __STACKTRACE__ 85 | end 86 | end 87 | 88 | defp keywords_to_change(doxygen) do 89 | %{ 90 | "PROJECT_NAME" => doxygen.project_name, 91 | "OUTPUT_DIRECTORY" => doxygen.doxygen_path, 92 | "TAB_SIZE" => "2", 93 | "BUILTIN_STL_SUPPORT" => "YES", 94 | "RECURSIVE" => "YES", 95 | "GENERATE_LATEX" => "NO", 96 | "INPUT" => Path.join(["c_src", doxygen.project_name]), 97 | "EXTRACT_STATIC" => "YES" 98 | } 99 | end 100 | 101 | defp update_doxyfile_keywords(doxygen, keywords_to_change) do 102 | doxyfile_path = doxygen.doxyfile_path 103 | 104 | File.stream!(doxyfile_path) 105 | |> Stream.map(fn line -> 106 | if comment?(line) do 107 | line 108 | else 109 | replace_keywords(line, keywords_to_change) 110 | end 111 | end) 112 | |> Enum.join() 113 | |> then(&File.write!(doxyfile_path, &1)) 114 | end 115 | 116 | defp comment?(line) do 117 | String.starts_with?(line, "#") 118 | end 119 | 120 | defp replace_keywords(line, keywords_to_change) do 121 | Enum.reduce(keywords_to_change, line, fn {keyword, value}, acc -> 122 | String.replace(acc, ~r/(#{keyword}\s*=)\s*(.*)\n/, "\\1 \"#{value}\"\n") 123 | end) 124 | end 125 | 126 | @doc """ 127 | Generates html doxygen documentation for the Bundlex project. Doxyfile must be generated before. 128 | """ 129 | @spec generate_doxygen_documentation(doxygen_t()) :: :ok 130 | def generate_doxygen_documentation(doxygen) do 131 | ensure_doxygen_dir_existence(doxygen) 132 | 133 | cmd_bundlex([doxygen.doxyfile_path]) 134 | end 135 | 136 | defp ensure_doxygen_dir_existence(doxygen) do 137 | if not File.exists?(doxygen.doxygen_path) do 138 | File.mkdir_p!(doxygen.doxygen_path) 139 | end 140 | 141 | File.touch!(Path.join(["doc", ".build"])) 142 | end 143 | 144 | @doc """ 145 | Generates page for the Bundlex project in the pages/doxygen directory. 146 | Page must be manually added to the docs extras in the mix.exs. 147 | Page contains only link to the doxygen html documentation. 148 | """ 149 | @spec generate_hex_page(doxygen_t()) :: :ok 150 | def generate_hex_page(doxygen) do 151 | pages_dirpath = Path.dirname(doxygen.page_path) 152 | 153 | if not File.exists?(pages_dirpath) do 154 | File.mkdir_p!(pages_dirpath) 155 | end 156 | 157 | [_doc | doxygen_path] = Path.split(doxygen.doxygen_path) 158 | 159 | html_filepath = Path.join(["."] ++ doxygen_path ++ ["html", "index.html"]) 160 | 161 | page = """ 162 | # Native code documentation 163 | [Doxygen documentation of the native code](#{html_filepath}) 164 | """ 165 | 166 | File.write!(doxygen.page_path, page) 167 | end 168 | end 169 | -------------------------------------------------------------------------------- /lib/bundlex/doxygen/error.ex: -------------------------------------------------------------------------------- 1 | defmodule Bundlex.Doxygen.Error do 2 | defexception [:message] 3 | end 4 | -------------------------------------------------------------------------------- /lib/bundlex/error.ex: -------------------------------------------------------------------------------- 1 | defmodule BundlexError do 2 | defexception [:message] 3 | end 4 | -------------------------------------------------------------------------------- /lib/bundlex/helper/erlang_helper.ex: -------------------------------------------------------------------------------- 1 | defmodule Bundlex.Helper.ErlangHelper do 2 | @moduledoc false 3 | # Module containing helper functions that ease determining path to locally- 4 | # installed Erlang. 5 | 6 | @doc """ 7 | Tries to determine paths to includes directory of locally installed Erlang. 8 | """ 9 | @spec get_includes(Bundlex.platform_t()) :: [String.t()] 10 | def get_includes(_platform) do 11 | [Path.join([:code.root_dir(), "usr", "include"])] 12 | end 13 | 14 | @doc """ 15 | Tries to determine paths to libs directory of locally installed Erlang. 16 | """ 17 | @spec get_lib_dirs(Bundlex.platform_t()) :: [String.t()] 18 | def get_lib_dirs(_platform) do 19 | [Path.join([:code.root_dir(), "usr", "lib"])] 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /lib/bundlex/helper/git_helper.ex: -------------------------------------------------------------------------------- 1 | defmodule Bundlex.Helper.GitHelper do 2 | @moduledoc false 3 | # Module simplifying interaction with Git. 4 | 5 | @doc """ 6 | Determines whether the [Git Large File Storage](https://git-lfs.github.com/) is 7 | activated on the current machine. 8 | """ 9 | @spec lfs_present? :: boolean 10 | def lfs_present? do 11 | Mix.shell().cmd("git config --get-regexp ^filter\.lfs\.", quiet: true) == 0 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /lib/bundlex/helper/mix_helper.ex: -------------------------------------------------------------------------------- 1 | defmodule Bundlex.Helper.MixHelper do 2 | @moduledoc false 3 | # Module containing helper functions that ease retrieving certain values from 4 | # Mix configuration files. 5 | 6 | use Bunch 7 | alias Bundlex.Output 8 | 9 | @doc """ 10 | Helper function for retrieving app name from mix.exs and failing if it was 11 | not found. 12 | """ 13 | @spec get_app! :: atom 14 | def get_app!() do 15 | case Mix.Project.config()[:app] do 16 | nil -> 17 | Output.raise( 18 | "Unable to determine app name, check if :app key is present in return value of project/0 in mix.exs" 19 | ) 20 | 21 | app -> 22 | app 23 | end 24 | end 25 | 26 | @doc """ 27 | Returns app for the given module. In case of failure fallbacks to `get_app!/0`. 28 | """ 29 | @spec get_app!(module) :: atom 30 | def get_app!(module) do 31 | Application.get_application(module) || get_app!() 32 | end 33 | 34 | @doc """ 35 | Returns path to the `priv` dir for given application. 36 | """ 37 | @spec get_priv_dir(application :: atom) :: String.t() 38 | def get_priv_dir(application \\ get_app!()) do 39 | # It seems that we have two methods to determine where Natives are located: 40 | # * `Mix.Project.build_path/0` 41 | # * `:code.priv_dir/1` 42 | # 43 | # Both seem to be unreliable, at least in Elixir 1.7: 44 | # 45 | # * we cannot call `Mix.Project.build_path/0` from `@on_load` handler as 46 | # there are race conditions and it seems that some processes from the 47 | # `:mix` app are not launched yet (yes, we tried to ensure that `:mix` 48 | # app is started, calling `Application.ensure_all_started(:mix)` prior 49 | # to calling `Mix.Project.build_path/0` causes deadlock; calling it 50 | # without ensuring that app is started terminates the whole app; adding 51 | # `:mix` to bundlex OTP applications does not seem to help either), 52 | # * it seems that when using releases, `Mix.Project.build_path/0` returns 53 | # different results in compile time and run time, 54 | # * moreover, it seems that the paths returned by `Mix.Project.build_path/0` 55 | # when using releases and `prod` env might have a `dev` suffix unless 56 | # a developer remembers to disable `:build_per_environment: setting, 57 | # * `:code.priv_dir/1` is not accessible in the compile time, but at least 58 | # it does not crash anything. 59 | # 60 | # As a result, we try to call `:code.priv_dir/1` first, and if it fails, 61 | # we are probably in the build time and need to fall back to 62 | # `Mix.Project.build_path/0`. Previously the check was reversed and it 63 | # caused crashes at least when using distillery >= 2.0 and Elixir 1.7. 64 | # 65 | # Think twice before you're going to be another person who spent many 66 | # hours on trying to figure out why such simple thing as determining 67 | # a path might be so hard. 68 | case :code.priv_dir(application) do 69 | {:error, :bad_name} -> 70 | [Mix.Project.build_path(), "lib", "#{application}", "priv"] |> Path.join() 71 | 72 | path -> 73 | path 74 | end 75 | end 76 | 77 | @doc """ 78 | Returns root directory of the currently compiled project. 79 | """ 80 | @spec get_project_dir() :: {:ok, binary} 81 | def get_project_dir() do 82 | {:ok, File.cwd!()} 83 | end 84 | 85 | @doc """ 86 | Returns root directory of the project of given application. 87 | """ 88 | @spec get_project_dir(application :: atom) :: {:ok, binary} | {:error, :unknown_application} 89 | def get_project_dir(application) do 90 | if application == get_app!() do 91 | get_project_dir() 92 | else 93 | case Mix.Project.deps_paths()[application] do 94 | nil -> {:error, :unknown_application} 95 | path -> {:ok, path} 96 | end 97 | end 98 | end 99 | end 100 | -------------------------------------------------------------------------------- /lib/bundlex/helper/path_helper.ex: -------------------------------------------------------------------------------- 1 | defmodule Bundlex.Helper.PathHelper do 2 | @moduledoc false 3 | # Module containing helper functions that ease traversing directories. 4 | 5 | @doc """ 6 | Fixes slashes in the given path to match convention used on current 7 | operating system. 8 | 9 | Internally all elixir functions use slash as a path separator, even if 10 | running on windows, and it's not a bug but a feature (lol). 11 | 12 | See https://github.com/elixir-lang/elixir/issues/1236 13 | """ 14 | @spec fix_slashes(String.t()) :: String.t() 15 | def fix_slashes(path) do 16 | case Bundlex.family() do 17 | :windows -> path |> String.replace("/", "\\") 18 | _family -> path 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /lib/bundlex/loader.ex: -------------------------------------------------------------------------------- 1 | defmodule Bundlex.Loader do 2 | @moduledoc """ 3 | Some utilities to ease loading of Bundlex-based NIFs. 4 | """ 5 | 6 | alias Bundlex.Helper.{MixHelper, PathHelper} 7 | 8 | @doc """ 9 | Binds the module to the specified NIF. 10 | 11 | Accepts keyword list, that may contain two arguments: 12 | - `:nif` - required, name of the NIF to be bound (the same as specified in `bundlex.exs`) 13 | - `:app` - application defining the NIF, defaults to the current application 14 | 15 | After `use`'ing this module you can utilize `defnif/1` and `defnifp/1` macros 16 | to create bindings to particular native functions. 17 | 18 | ## Example 19 | 20 | defmodule My.Native.Module do 21 | use Bundlex.Loader, nif: :my_nif 22 | 23 | defnif native_function(arg1, arg2, arg3) 24 | 25 | def normal_function(arg1, arg2) do 26 | # ... 27 | end 28 | 29 | defnifp private_native_function(arg1, arg2) 30 | 31 | end 32 | """ 33 | defmacro __using__(keyword) do 34 | quote do 35 | import unquote(__MODULE__), only: [defnif: 1, defnifp: 1] 36 | @before_compile unquote(__MODULE__) 37 | @bundlex_nif_name unquote(keyword |> Keyword.fetch!(:nif)) 38 | @bundlex_app unquote(keyword |> Keyword.get(:app)) 39 | Module.register_attribute(__MODULE__, :bundlex_defnifs, accumulate: true) 40 | end 41 | end 42 | 43 | @doc false 44 | @spec __before_compile__(Macro.Env.t()) :: :ok 45 | def __before_compile__(%{module: module} = env) do 46 | funs = Module.delete_attribute(module, :bundlex_defnifs) 47 | nif_name = Module.delete_attribute(module, :bundlex_nif_name) 48 | app = Module.delete_attribute(module, :bundlex_app) 49 | 50 | defs = 51 | funs 52 | |> Enum.map(fn fun -> 53 | {name, _location, args} = fun 54 | 55 | args = 56 | args 57 | |> Enum.map(fn 58 | {:\\, _meta, [arg, _default]} -> arg 59 | arg -> arg 60 | end) 61 | 62 | quote do 63 | # credo:disable-for-next-line Credo.Check.Readability.Specs 64 | def unquote(fun) do 65 | :erlang.nif_error( 66 | "Nif fail: #{unquote(module)}.#{unquote(name)}/#{length(unquote(args))}" 67 | ) 68 | end 69 | end 70 | end) 71 | 72 | nif_module_content = 73 | quote do 74 | @moduledoc false 75 | require unquote(__MODULE__) 76 | 77 | @on_load :load_nif 78 | 79 | @spec load_nif() :: :ok | no_return() 80 | def load_nif() do 81 | unquote(__MODULE__).load_nif!(unquote(app), unquote(nif_name)) 82 | end 83 | 84 | unquote(defs) 85 | end 86 | 87 | Module.create(module |> Module.concat(Nif), nif_module_content, env) 88 | :ok 89 | end 90 | 91 | @doc """ 92 | Generates function bound to the native implementation. This module has to be 93 | `use`d for this macro to work. 94 | 95 | Function name should correspond to the native one. 96 | 97 | See `__using__/1` for examples. 98 | """ 99 | defmacro defnif({name, _pos, args} = definition) do 100 | quote do 101 | @bundlex_defnifs unquote(Macro.escape(definition)) 102 | @compile {:inline, [unquote({name, length(args)})]} 103 | defdelegate unquote(definition), to: __MODULE__.Nif 104 | end 105 | end 106 | 107 | @doc """ 108 | Works the same way as `defnif/1`, but generates private function. This module 109 | has to be `use`d for this macro to work. 110 | 111 | See `__using__/1` for examples. 112 | """ 113 | defmacro defnifp({name, _pos, args} = definition) do 114 | quote do 115 | @bundlex_defnifs unquote(Macro.escape(definition)) 116 | @compile {:inline, [unquote({name, length(args)})]} 117 | defp unquote(definition) do 118 | __MODULE__.Nif.unquote(definition) 119 | end 120 | end 121 | end 122 | 123 | @doc """ 124 | Binds calling module to NIF `nif_name` from application `app`. 125 | 126 | Second argument has to be an atom, the same as name of the NIF in the bundlex 127 | project. 128 | 129 | Invoked internally by `__using__/1` macro, which is the preferred way of loading 130 | NIFs. 131 | """ 132 | defmacro load_nif!(app \\ nil, nif_name) do 133 | quote do 134 | app = unquote(app || MixHelper.get_app!()) 135 | nif_name = unquote(nif_name) 136 | 137 | path = Bundlex.build_path(app, nif_name, :nif) 138 | 139 | with :ok <- :erlang.load_nif(path |> PathHelper.fix_slashes() |> to_charlist(), 0) do 140 | :ok 141 | else 142 | {:error, {reason, text}} -> 143 | require Logger 144 | 145 | Logger.error(""" 146 | Bundlex cannot load nif #{inspect(nif_name)} of app #{inspect(app)} 147 | from "#{path}", check bundlex.exs file for information about nifs. 148 | Reason: #{inspect(reason)}, #{to_string(text)} 149 | """) 150 | 151 | :abort 152 | end 153 | end 154 | end 155 | end 156 | -------------------------------------------------------------------------------- /lib/bundlex/native.ex: -------------------------------------------------------------------------------- 1 | defmodule Bundlex.Native do 2 | @moduledoc """ 3 | Module responsible for parsing and processing natives' configurations. 4 | """ 5 | 6 | use Bunch 7 | 8 | require Logger 9 | alias Bundlex.Helper.ErlangHelper 10 | alias Bundlex.{Output, Platform, Project} 11 | alias Bundlex.Project.Preprocessor 12 | alias Bundlex.Toolchain.Common.Unix.OSDeps 13 | 14 | @type t :: %__MODULE__{ 15 | name: Project.native_name(), 16 | app: Application.app(), 17 | type: :native | :lib, 18 | includes: [String.t()], 19 | libs: [String.t()], 20 | lib_dirs: [String.t()], 21 | os_deps: [Project.os_dep()], 22 | pkg_configs: [String.t()], 23 | sources: [String.t()], 24 | deps: [t], 25 | compiler_flags: [String.t()], 26 | linker_flags: [String.t()], 27 | language: Project.native_language(), 28 | interface: Project.native_interface() | nil, 29 | preprocessors: [Preprocessor.t()] 30 | } 31 | 32 | @enforce_keys [:name, :type] 33 | 34 | defstruct name: nil, 35 | app: nil, 36 | type: nil, 37 | includes: [], 38 | libs: [], 39 | lib_dirs: [], 40 | os_deps: [], 41 | pkg_configs: [], 42 | sources: [], 43 | deps: [], 44 | compiler_flags: [], 45 | linker_flags: [], 46 | language: :c, 47 | interface: nil, 48 | preprocessors: [] 49 | 50 | @project_keys Project.native_config_keys() 51 | 52 | @native_type_keys %{native: :natives, lib: :libs} 53 | 54 | @doc """ 55 | Parses natives and generates compiler commands. 56 | """ 57 | @spec resolve_natives(Project.t(), Bundlex.platform_t()) :: 58 | {:ok, compiler_commands :: [String.t()]} 59 | | {:error, 60 | {application :: atom, 61 | {:unknown_fields, [field :: atom]} 62 | | {:no_sources_in_native, native_name :: atom} 63 | | :invalid_project_specification 64 | | {:no_bundlex_project_in_file, path :: binary()} 65 | | :unknown_application}} 66 | def resolve_natives(project, platform) do 67 | case get_native_configs(project) do 68 | [] -> 69 | Output.info("No natives found") 70 | {:ok, []} 71 | 72 | native_configs -> 73 | erlang = %{ 74 | includes: ErlangHelper.get_includes(platform), 75 | lib_dirs: ErlangHelper.get_lib_dirs(platform) 76 | } 77 | 78 | Output.info( 79 | "Building natives: #{native_configs |> Enum.map(& &1.name) |> Enum.uniq() |> Enum.join(", ")}" 80 | ) 81 | 82 | native_configs 83 | |> Bunch.Enum.try_flat_map(&resolve_native(&1, erlang, project.src_path, platform)) 84 | end 85 | end 86 | 87 | defp resolve_native(config, erlang, src_path, platform) do 88 | with {:ok, native} <- parse_native(config, src_path) do 89 | %__MODULE__{} = 90 | native = Enum.reduce(native.preprocessors, native, & &1.preprocess_native(&2)) 91 | 92 | native = 93 | case native do 94 | %__MODULE__{type: :native, interface: :cnode} = native -> 95 | native 96 | |> Map.update!(:libs, &["pthread", "ei" | &1]) 97 | |> Map.update!(:lib_dirs, &(erlang.lib_dirs ++ &1)) 98 | 99 | %__MODULE__{} = native -> 100 | native 101 | end 102 | |> Map.update!(:includes, &(erlang.includes ++ &1)) 103 | |> merge_deps() 104 | |> Map.update!(:sources, &Enum.uniq/1) 105 | |> Map.update!(:deps, fn deps -> Enum.uniq_by(deps, &{&1.app, &1.name}) end) 106 | 107 | native = 108 | if native.pkg_configs != [] do 109 | Output.warn("`pkg_configs` option has been deprecated. Please use `os_deps` option.") 110 | %{native | os_deps: [{:pkg_config, native.pkg_configs} | native.os_deps]} 111 | else 112 | native 113 | end 114 | 115 | native_with_resolved_os_deps = OSDeps.resolve_os_deps(native) 116 | 117 | commands = 118 | Platform.get_module(platform).toolchain_module().compiler_commands( 119 | native_with_resolved_os_deps 120 | ) 121 | 122 | {:ok, commands} 123 | end 124 | end 125 | 126 | defp parse_native(config, src_path) do 127 | {config, meta} = config |> Map.pop(:config) 128 | {preprocessors, config} = config |> Keyword.pop(:preprocessor, []) 129 | preprocessors = preprocessors |> Bunch.listify() 130 | 131 | config = 132 | Enum.reduce(preprocessors, config, & &1.preprocess_native_config(meta.name, meta.app, &2)) 133 | 134 | {deps, config} = config |> Keyword.pop(:deps, []) 135 | interface = config |> Keyword.get(:interface) 136 | 137 | {src_base, config} = config |> Keyword.pop(:src_base, "#{meta.app}") 138 | 139 | withl fields: [] <- config |> Keyword.keys() |> Enum.reject(&(&1 in @project_keys)), 140 | do: native = (config ++ Enum.to_list(meta)) |> __struct__(), 141 | no_src: false <- native.sources |> Enum.empty?(), 142 | deps: {:ok, parsed_deps} <- parse_deps(deps, interface) do 143 | native = 144 | %__MODULE__{native | deps: parsed_deps, preprocessors: preprocessors} 145 | |> Map.update!(:includes, &[Path.join([src_path, src_base, ".."]) | &1]) 146 | |> Map.update!(:sources, fn src -> 147 | src |> Enum.map(&Path.join([src_path, src_base, &1])) 148 | end) 149 | 150 | {:ok, native} 151 | else 152 | fields: fields -> {:error, {meta.app, {:unknown_fields, fields}}} 153 | no_src: true -> {:error, {meta.app, {:no_sources_in_native, native.name}}} 154 | deps: error -> error 155 | end 156 | end 157 | 158 | defp get_native_configs(project, types \\ [:lib, :native]) do 159 | types 160 | |> Bunch.listify() 161 | |> Enum.flat_map(fn type -> 162 | project.config 163 | |> Keyword.get(@native_type_keys[type], []) 164 | |> Enum.map(fn {name, config} -> 165 | %{config: config, name: name, type: type, app: project.app} 166 | end) 167 | end) 168 | end 169 | 170 | defp parse_deps(deps, interface) do 171 | deps 172 | |> Bunch.Enum.try_flat_map(fn {app, natives} -> 173 | parse_app_libs(app, natives |> Bunch.listify() |> MapSet.new(), interface) 174 | end) 175 | end 176 | 177 | defp parse_app_libs(app, names, interface) do 178 | withl project: {:ok, project} <- app |> Project.get(), 179 | do: libs = find_libs(project, names), 180 | libs: {:ok, libs} <- parse_libs(libs, project.src_path) do 181 | filter_libs(libs, names, interface) 182 | else 183 | project: {:error, reason} -> {:error, {app, reason}} 184 | libs: error -> error 185 | end 186 | end 187 | 188 | defp find_libs(project, names) do 189 | project |> get_native_configs(:lib) |> Enum.filter(&(&1.name in names)) 190 | end 191 | 192 | defp filter_libs(libs, names, interface) do 193 | libs = Enum.filter(libs, &(&1.interface in [nil, interface])) 194 | diff = MapSet.difference(names, MapSet.new(libs, & &1.name)) 195 | 196 | if diff |> Enum.empty?() do 197 | {:ok, libs} 198 | else 199 | {:error, {:libs_not_found, diff |> Enum.to_list()}} 200 | end 201 | end 202 | 203 | defp parse_libs(libs, src_path) do 204 | Bunch.Enum.try_map(libs, &parse_native(&1, src_path)) 205 | end 206 | 207 | defp merge_deps(native) do 208 | native.deps |> Enum.map(&merge_deps/1) |> Enum.reduce(native, &merge_dep/2) 209 | end 210 | 211 | defp merge_dep(%__MODULE__{type: :lib} = dependency, %__MODULE__{} = native) do 212 | Map.merge( 213 | native, 214 | Map.take(dependency, [ 215 | :includes, 216 | :libs, 217 | :lib_dirs, 218 | :os_deps, 219 | :pkg_configs, 220 | :linker_flags, 221 | :deps 222 | ]), 223 | fn _k, v1, v2 -> v2 ++ v1 end 224 | ) 225 | end 226 | end 227 | -------------------------------------------------------------------------------- /lib/bundlex/output.ex: -------------------------------------------------------------------------------- 1 | defmodule Bundlex.Output do 2 | @moduledoc false 3 | 4 | @spec info(String.t()) :: :ok 5 | def info(msg) do 6 | Mix.shell().info("Bundlex: " <> msg) 7 | end 8 | 9 | @spec warn(String.t()) :: :ok 10 | def warn(msg) do 11 | IO.warn("Bundlex: " <> msg, []) 12 | end 13 | 14 | @spec raise(binary()) :: no_return() 15 | def raise(msg) do 16 | Mix.raise("Bundlex: " <> msg) 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /lib/bundlex/platform.ex: -------------------------------------------------------------------------------- 1 | defmodule Bundlex.Platform do 2 | @moduledoc false 3 | alias Bundlex.Output 4 | 5 | @type name_t :: atom 6 | @type family_name_t :: atom 7 | 8 | @callback extra_otp_configure_options() :: [] | [String.t()] 9 | @callback required_env_vars() :: [] | [String.t()] 10 | @callback patches_to_apply() :: [] | [String.t()] 11 | @callback toolchain_module() :: module 12 | 13 | defmacro __using__(_args) do 14 | quote do 15 | @behaviour unquote(__MODULE__) 16 | 17 | @impl unquote(__MODULE__) 18 | def extra_otp_configure_options() do 19 | [] 20 | end 21 | 22 | @impl unquote(__MODULE__) 23 | def required_env_vars() do 24 | [] 25 | end 26 | 27 | @impl unquote(__MODULE__) 28 | def patches_to_apply() do 29 | [] 30 | end 31 | 32 | defoverridable unquote(__MODULE__) 33 | end 34 | end 35 | 36 | @doc """ 37 | Detects target platform. 38 | 39 | In case of success returns platform name 40 | 41 | Otherwise raises Mix error. 42 | """ 43 | @spec get_target!() :: name_t 44 | 45 | case System.fetch_env("CROSSCOMPILE") do 46 | :error -> 47 | def get_target!() do 48 | case :os.type() do 49 | {:win32, _} -> 50 | {:ok, reg} = :win32reg.open([:read]) 51 | 52 | :ok = 53 | :win32reg.change_key( 54 | reg, 55 | ~c"\\hklm\\SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion" 56 | ) 57 | 58 | {:ok, build} = :win32reg.value(reg, ~c"BuildLabEx") 59 | 60 | platform_name = 61 | if build |> to_string |> String.contains?("amd64") do 62 | :windows64 63 | else 64 | :windows32 65 | end 66 | 67 | :ok = :win32reg.close(reg) 68 | 69 | platform_name 70 | 71 | {:unix, :linux} -> 72 | :linux 73 | 74 | {:unix, :freebsd} -> 75 | :freebsd 76 | 77 | {:unix, :darwin} -> 78 | :macosx 79 | 80 | other -> 81 | # TODO add detection for more platforms 82 | Output.raise( 83 | "Unable to detect current platform. Erlang returned #{inspect(other)} which I don't know how to handle." 84 | ) 85 | end 86 | end 87 | 88 | {:ok, _crosscompile} -> 89 | case System.fetch_env("NERVES_APP") do 90 | {:ok, _app} -> 91 | def get_target!(), do: :nerves 92 | 93 | :error -> 94 | Output.info( 95 | "Cross-compiling without using Nerves. Make sure necessary environment variables are set correctly." 96 | ) 97 | 98 | def get_target!(), do: :custom 99 | end 100 | end 101 | 102 | @spec family(name_t) :: family_name_t 103 | def family(:windows32), do: :windows 104 | def family(:windows64), do: :windows 105 | def family(:linux), do: :unix 106 | def family(:macosx), do: :unix 107 | def family(:freebsd), do: :unix 108 | def family(:nerves), do: :unix 109 | def family(:custom), do: :custom 110 | 111 | @spec get_module(family_name_t) :: module 112 | def get_module(:windows32), do: Bundlex.Platform.Windows32 113 | def get_module(:windows64), do: Bundlex.Platform.Windows64 114 | def get_module(:macosx), do: Bundlex.Platform.MacOSX 115 | def get_module(:linux), do: Bundlex.Platform.Linux 116 | def get_module(:freebsd), do: Bundlex.Platform.Freebsd 117 | def get_module(:nerves), do: Bundlex.Platform.Custom 118 | def get_module(:custom), do: Bundlex.Platform.Custom 119 | end 120 | -------------------------------------------------------------------------------- /lib/bundlex/platform/freebsd.ex: -------------------------------------------------------------------------------- 1 | defmodule Bundlex.Platform.Freebsd do 2 | @moduledoc false 3 | use Bundlex.Platform 4 | 5 | @impl true 6 | def toolchain_module() do 7 | Bundlex.Toolchain.Clang 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /lib/bundlex/platform/linux.ex: -------------------------------------------------------------------------------- 1 | defmodule Bundlex.Platform.Linux do 2 | @moduledoc false 3 | use Bundlex.Platform 4 | 5 | @impl true 6 | def toolchain_module() do 7 | Bundlex.Toolchain.GCC 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /lib/bundlex/platform/macosx.ex: -------------------------------------------------------------------------------- 1 | defmodule Bundlex.Platform.MacOSX do 2 | @moduledoc false 3 | use Bundlex.Platform 4 | 5 | @impl true 6 | def toolchain_module() do 7 | Bundlex.Toolchain.XCode 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /lib/bundlex/platform/nerves.ex: -------------------------------------------------------------------------------- 1 | defmodule Bundlex.Platform.Custom do 2 | @moduledoc false 3 | use Bundlex.Platform 4 | 5 | @impl true 6 | def toolchain_module() do 7 | Bundlex.Toolchain.Custom 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /lib/bundlex/platform/windows32.ex: -------------------------------------------------------------------------------- 1 | defmodule Bundlex.Platform.Windows32 do 2 | @moduledoc false 3 | use Bundlex.Platform 4 | 5 | @impl true 6 | def toolchain_module() do 7 | Bundlex.Toolchain.VisualStudio 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /lib/bundlex/platform/windows64.ex: -------------------------------------------------------------------------------- 1 | defmodule Bundlex.Platform.Windows64 do 2 | @moduledoc false 3 | use Bundlex.Platform 4 | 5 | @impl true 6 | def toolchain_module() do 7 | Bundlex.Toolchain.VisualStudio 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /lib/bundlex/port.ex: -------------------------------------------------------------------------------- 1 | defmodule Bundlex.Port do 2 | @moduledoc """ 3 | Utilities to ease interaction with Ports. 4 | """ 5 | 6 | alias Bundlex.Helper.MixHelper 7 | alias Bundlex.Project 8 | 9 | @doc """ 10 | Spawns Port `native_name` from application of calling module. 11 | Returned result is compatible with standard Port API. 12 | """ 13 | defmacro open(native_name, args \\ []) do 14 | app = MixHelper.get_app!(__CALLER__.module) 15 | 16 | quote do 17 | unquote(__MODULE__).open(unquote(app), unquote(native_name), unquote(args)) 18 | end 19 | end 20 | 21 | @spec open(Application.app(), Project.native_name(), [String.t()]) :: port() 22 | def open(app, native_name, args) do 23 | Port.open( 24 | {:spawn_executable, Bundlex.build_path(app, native_name, :port)}, 25 | args: args 26 | ) 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /lib/bundlex/project.ex: -------------------------------------------------------------------------------- 1 | defmodule Bundlex.Project do 2 | @moduledoc """ 3 | Behaviour that should be implemented by each project using Bundlex in the 4 | `bundlex.exs` file. 5 | """ 6 | use Bunch 7 | alias Bundlex.Helper.MixHelper 8 | alias __MODULE__.{Preprocessor, Store} 9 | 10 | @src_dir_name "c_src" 11 | @bundlex_file_name "bundlex.exs" 12 | 13 | @type native_name :: atom 14 | @type native_interface :: :nif | :cnode | :port 15 | @type native_language :: :c | :cpp 16 | 17 | @type os_dep_provider :: 18 | :pkg_config 19 | | {:pkg_config, pkg_configs :: String.t() | [String.t()]} 20 | | {:precompiled, url :: String.t()} 21 | | {:precompiled, url :: String.t(), libs :: String.t() | [String.t()]} 22 | 23 | @type os_dep :: {name :: atom, os_dep_provider | [os_dep_provider]} 24 | 25 | @typedoc """ 26 | Type describing configuration of a native. 27 | 28 | Configuration of each native may contain following options: 29 | * `sources` - C files to be compiled (at least one must be provided). 30 | * `preprocessors` - Modules that will pre-process the native. They may change this configuration, for example 31 | by adding new keys. An example of preprocessor is [Unifex](https://hexdocs.pm/unifex/Unifex.html). 32 | See `Bundlex.Project.Preprocessor` for more details. 33 | * `interface` - Interface used to integrate with Elixir code. The following interfaces are available: 34 | * :nif - dynamically linked to the Erlang VM (see [Erlang docs](http://erlang.org/doc/man/erl_nif.html)) 35 | * :cnode - executed as separate OS processes, accessed through sockets (see [Erlang docs](http://erlang.org/doc/man/ei_connect.html)) 36 | * :port - executed as separate OS processes (see [Elixir Port docs](https://hexdocs.pm/elixir/Port.html)) 37 | Specifying no interface is valid only for libs. 38 | * `deps` - Dependencies in the form of `{app, lib_name}`, where `app` 39 | is the application name of the dependency, and `lib_name` is the name of lib 40 | specified in Bundlex project of this dependency. Empty list by default. See _Dependencies_ section below 41 | for details. 42 | * `os_deps` - List of external OS dependencies. It's a keyword list, where each key is the 43 | dependency name and the value is a provider or a list of them. In the latter case, subsequent 44 | providers from the list will be tried until one of them succeeds. A provider may be one of: 45 | - `pkg_config` - Resolves the dependency via `pkg-config`. Can be either `{:pkg_config, pkg_configs}` 46 | or just `:pkg_config`, in which case the dependency name will be used as the pkg_config name. 47 | - `precompiled` - Downloads the dependency from a given url and sets appropriate compilation 48 | and linking flags. Can be either `{:precompiled, url, libs}` or `{:precompiled, url}`, in which 49 | case the dependency name will be used as the lib name. 50 | Precompiled dependencies can be disabled via configuration globally: 51 | 52 | ```elixir 53 | config :bundlex, :disable_precompiled_os_deps, true 54 | ``` 55 | 56 | or for given applications (Mix projects), for example: 57 | 58 | ```elixir 59 | config :bundlex, :disable_precompiled_os_deps, 60 | apps: [:my_application, :another_application] 61 | ``` 62 | 63 | Note that this will affect the natives and libs defined in the `bundlex.exs` files of specified 64 | applications only, not in their dependencies. 65 | 66 | Check `t:os_dep/0` for details. 67 | * `pkg_configs` - (deprecated, use `os_deps` instead) Names of libraries for which the appropriate flags will be 68 | obtained using pkg-config (empty list by default). 69 | * `language` - Language of native. `:c` or `:cpp` may be chosen (`:c` by default). 70 | * `src_base` - Native files should reside in `project_root/c_src/` 71 | (application name by default). 72 | * `includes` - Paths to look for header files (empty list by default). 73 | * `lib_dirs` - Absolute paths to look for libraries (empty list by default). 74 | * `libs` - Names of libraries to link (empty list by default). 75 | * `compiler_flags` - Custom flags for compiler. Default `-std` flag for `:c` is `-std=c11` and for `:cpp` is `-std=c++17`. 76 | * `linker_flags` - Custom flags for linker. 77 | """ 78 | 79 | native_config_type = 80 | quote do 81 | [ 82 | sources: [String.t()], 83 | includes: [String.t()], 84 | lib_dirs: [String.t()], 85 | libs: [String.t()], 86 | os_deps: [os_dep], 87 | pkg_configs: [String.t()], 88 | deps: [{Application.app(), native_name | [native_name]}], 89 | src_base: String.t(), 90 | compiler_flags: [String.t()], 91 | linker_flags: [String.t()], 92 | language: :c | :cpp, 93 | interface: native_interface | [native_interface], 94 | preprocessor: [Preprocessor.t()] | Preprocessor.t() 95 | ] 96 | end 97 | 98 | @type native_config :: unquote(native_config_type) 99 | 100 | @spec native_config_keys :: [atom] 101 | def native_config_keys, do: unquote(Keyword.keys(native_config_type)) 102 | 103 | @typedoc """ 104 | Type describing input project configuration. 105 | 106 | It's a keyword list, where natives and libs can be specified. Libs are 107 | native packages that are compiled as static libraries and linked to natives 108 | that have them specified in `deps` field of their configuration. 109 | """ 110 | @type config :: [{:natives | :libs, [{native_name, native_config}]}] 111 | 112 | @doc """ 113 | Callback returning project configuration. 114 | """ 115 | @callback project() :: config 116 | 117 | defmacro __using__(_args) do 118 | quote do 119 | @behaviour unquote(__MODULE__) 120 | @doc false 121 | @spec __bundlex_project__() :: true 122 | def __bundlex_project__, do: true 123 | 124 | @doc false 125 | @spec __src_path__() :: Path.t() 126 | def __src_path__, do: Path.join(__DIR__, unquote(@src_dir_name)) 127 | end 128 | end 129 | 130 | @typedoc """ 131 | Struct representing bundlex project. 132 | 133 | Contains the following fields: 134 | - `:config` - project configuration 135 | - `:src_path` - path to the native sources 136 | - `:module` - bundlex project module 137 | - `:app` - application that exports project 138 | """ 139 | @type t :: %__MODULE__{ 140 | config: config, 141 | src_path: String.t(), 142 | module: module, 143 | app: atom 144 | } 145 | 146 | @enforce_keys [:config, :src_path, :module, :app] 147 | defstruct @enforce_keys 148 | 149 | @doc """ 150 | Determines if `module` is a bundlex project module. 151 | """ 152 | @spec project_module?(module) :: boolean 153 | def project_module?(module) do 154 | function_exported?(module, :__bundlex_project__, 0) and module.__bundlex_project__() 155 | end 156 | 157 | @doc """ 158 | Returns the project struct of given application. 159 | 160 | If the module has not been loaded yet, it is loaded from 161 | `project_dir/#{@bundlex_file_name}` file. 162 | """ 163 | @spec get(application :: atom) :: 164 | {:ok, t} 165 | | {:error, 166 | :invalid_project_specification 167 | | {:no_bundlex_project_in_file, path :: binary()} 168 | | :unknown_application} 169 | def get(application \\ MixHelper.get_app!()) do 170 | project = Store.get_project(application) 171 | 172 | if project do 173 | {:ok, project} 174 | else 175 | with {:ok, module} <- load(application), 176 | {:ok, config} <- parse_project_config(module.project()) do 177 | project = %__MODULE__{ 178 | config: config, 179 | src_path: module.__src_path__(), 180 | module: module, 181 | app: application 182 | } 183 | 184 | Store.store_project(application, project) 185 | {:ok, project} 186 | end 187 | end 188 | end 189 | 190 | @spec load(application :: atom) :: 191 | {:ok, module} 192 | | {:error, {:no_bundlex_project_in_file, path :: binary()} | :unknown_application} 193 | defp load(application) do 194 | with {:ok, dir} <- MixHelper.get_project_dir(application) do 195 | bundlex_file_path = dir |> Path.join(@bundlex_file_name) 196 | modules = Code.require_file(bundlex_file_path) |> Keyword.keys() 197 | 198 | modules 199 | |> Enum.find(&project_module?/1) 200 | |> Bunch.error_if_nil({:no_bundlex_project_in_file, bundlex_file_path}) 201 | end 202 | end 203 | 204 | defp parse_project_config(config) do 205 | if Keyword.keyword?(config) do 206 | config = 207 | config 208 | |> delistify_interfaces(:libs) 209 | |> delistify_interfaces(:natives) 210 | 211 | {:ok, config} 212 | else 213 | {:error, :invalid_project_specification} 214 | end 215 | end 216 | 217 | defp delistify_interfaces(input_config, native_type) do 218 | natives = Keyword.get(input_config, native_type, []) 219 | 220 | natives = 221 | natives 222 | |> Enum.flat_map(fn {name, config} -> 223 | config 224 | |> Keyword.get(:interface, nil) 225 | |> Bunch.listify() 226 | |> Enum.map(&{name, Keyword.put(config, :interface, &1)}) 227 | end) 228 | 229 | Keyword.put(input_config, native_type, natives) 230 | end 231 | end 232 | -------------------------------------------------------------------------------- /lib/bundlex/project/preprocessor.ex: -------------------------------------------------------------------------------- 1 | defmodule Bundlex.Project.Preprocessor do 2 | @moduledoc """ 3 | Behaviour for preprocessing Bundlex projects. 4 | 5 | Precompiling may involve either generating additional resources or altering the project itself. 6 | Currently, preprocessing native configuration (`c:preprocess_native_config/3`) 7 | and parsed natives (`c:preprocess_native/1`) is supported. 8 | """ 9 | alias Bundlex.{Native, Project} 10 | 11 | @type t :: module 12 | 13 | @callback preprocess_native_config( 14 | name :: atom, 15 | app :: atom, 16 | config :: Project.native_config() 17 | ) :: 18 | Project.native_config() 19 | @callback preprocess_native(native :: Native.t()) :: Native.t() 20 | end 21 | -------------------------------------------------------------------------------- /lib/bundlex/project/store.ex: -------------------------------------------------------------------------------- 1 | defmodule Bundlex.Project.Store do 2 | @moduledoc false 3 | use Agent 4 | alias Bundlex.Project 5 | 6 | @spec start_link(any) :: Agent.on_start() 7 | def start_link(_opts) do 8 | Agent.start_link(fn -> %{} end, name: __MODULE__) 9 | end 10 | 11 | @spec get_project(application :: atom) :: Project.t() | nil 12 | def get_project(application) do 13 | Agent.get(__MODULE__, & &1[application]) 14 | end 15 | 16 | @spec store_project(application :: atom, Project.t()) :: :ok 17 | def store_project(application, project) do 18 | Agent.update(__MODULE__, &Map.put(&1, application, project)) 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /lib/bundlex/toolchain.ex: -------------------------------------------------------------------------------- 1 | defmodule Bundlex.Toolchain do 2 | @moduledoc false 3 | 4 | alias Bundlex.Helper.MixHelper 5 | alias Bundlex.Project 6 | 7 | @doc """ 8 | Invokes commands that should be called before whole compilation process 9 | for given platform. 10 | 11 | Implementations should call `Output.raise/1` in case of failure which will 12 | cause breaking the compilation process. 13 | 14 | In case of success implementations should return list of commands to be 15 | called upon compilation. 16 | 17 | Default implementation does nothing. 18 | """ 19 | @callback before_all!(atom) :: [] | [String.t()] 20 | 21 | @doc """ 22 | Builds list of compiler commands valid for certain toolchain. 23 | """ 24 | @callback compiler_commands(Bundlex.Native.t()) :: [String.t()] 25 | 26 | defmacro __using__(_) do 27 | quote location: :keep do 28 | @behaviour unquote(__MODULE__) 29 | alias unquote(__MODULE__) 30 | 31 | # Default implementations 32 | 33 | @impl unquote(__MODULE__) 34 | def before_all!(_platform), do: [] 35 | 36 | defoverridable before_all!: 1 37 | end 38 | end 39 | 40 | @spec output_path(Application.app(), Project.native_interface()) :: Path.t() 41 | def output_path(app, native_interface) do 42 | interface_str = 43 | case native_interface do 44 | nil -> "" 45 | interface -> "#{interface}" 46 | end 47 | 48 | MixHelper.get_priv_dir(app) |> Path.join("bundlex") |> Path.join(interface_str) 49 | end 50 | 51 | @spec output_path(Application.app(), Project.native_name(), Project.native_interface()) :: 52 | Path.t() 53 | def output_path(app, native_name, native_interface) do 54 | output_path(app, native_interface) |> Path.join("#{native_name}") 55 | end 56 | 57 | @spec bundlex_shared_path() :: Path.t() 58 | def bundlex_shared_path() do 59 | Path.join(MixHelper.get_priv_dir(:bundlex), "shared") 60 | end 61 | end 62 | -------------------------------------------------------------------------------- /lib/bundlex/toolchain/clang.ex: -------------------------------------------------------------------------------- 1 | defmodule Bundlex.Toolchain.Clang do 2 | @moduledoc false 3 | 4 | use Bundlex.Toolchain 5 | alias Bundlex.Native 6 | alias Bundlex.Toolchain.Common.{Compilers, Unix} 7 | 8 | @compilers %Compilers{c: "clang", cpp: "clang++"} 9 | 10 | @impl Toolchain 11 | def compiler_commands(native) do 12 | {cflags, lflags} = 13 | case native do 14 | %Native{type: :native, interface: :nif} -> {"-fPIC", "-rdynamic -shared"} 15 | %Native{type: :lib} -> {"-fPIC", ""} 16 | %Native{} -> {"", ""} 17 | end 18 | 19 | compiler = @compilers |> Map.get(native.language) 20 | 21 | Unix.compiler_commands( 22 | native, 23 | "#{compiler} #{cflags}", 24 | "#{compiler} #{lflags}", 25 | native.language, 26 | wrap_deps: &"-Wl,--whole-archive #{&1} -Wl,--no-whole-archive" 27 | ) 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /lib/bundlex/toolchain/common/compilers.ex: -------------------------------------------------------------------------------- 1 | defmodule Bundlex.Toolchain.Common.Compilers do 2 | @moduledoc false 3 | # Provides few utilities related to various compilation methods 4 | 5 | @enforce_keys [:c, :cpp] 6 | defstruct @enforce_keys 7 | 8 | @doc """ 9 | Provides compiler flag specyfying default language standard, each one for every supported language 10 | """ 11 | @spec get_default_std_flag(:c | :cpp) :: String.t() 12 | def get_default_std_flag(:cpp) do 13 | "-std=c++17" 14 | end 15 | 16 | def get_default_std_flag(:c) do 17 | "-std=c11" 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /lib/bundlex/toolchain/common/unix.ex: -------------------------------------------------------------------------------- 1 | defmodule Bundlex.Toolchain.Common.Unix do 2 | @moduledoc false 3 | 4 | use Bunch 5 | alias Bundlex.{Native, Project, Toolchain} 6 | alias Bundlex.Toolchain.Common.Compilers 7 | 8 | @spec compiler_commands( 9 | Native.t(), 10 | compile :: String.t(), 11 | link :: String.t(), 12 | lang :: Project.native_language(), 13 | options :: Keyword.t() 14 | ) :: [String.t()] 15 | def compiler_commands(native, compile, link, lang, options \\ []) do 16 | includes = native.includes |> paths("-I") 17 | 18 | compiler_flags = resolve_compiler_flags(native.compiler_flags, native.interface, lang) 19 | output = Toolchain.output_path(native.app, native.name, native.interface) 20 | output_obj = output <> "_obj" 21 | 22 | objects = 23 | native.sources 24 | |> Enum.map(fn source -> 25 | """ 26 | #{Path.join(output_obj, source |> Path.basename())}_\ 27 | #{:crypto.hash(:sha, source) |> Base.encode16()}.o\ 28 | """ 29 | end) 30 | 31 | compile_commands = 32 | native.sources 33 | |> Enum.zip(objects) 34 | |> Enum.map(fn {source, object} -> 35 | """ 36 | #{compile} -Wall -Wextra -c -O2 -g #{compiler_flags} \ 37 | -o #{path(object)} #{includes} #{path(source)} 38 | """ 39 | end) 40 | 41 | ["mkdir -p #{path(output_obj)}"] ++ 42 | compile_commands ++ link_commands(native, link, output, objects, options) 43 | end 44 | 45 | defp resolve_compiler_flags(compiler_flags, interface, lang) do 46 | compiler_flags 47 | |> add_interface_macro_flag(interface) 48 | |> maybe_add_std_flag(lang) 49 | |> Enum.join(" ") 50 | end 51 | 52 | defp add_interface_macro_flag(compiler_flags, nil) do 53 | compiler_flags 54 | end 55 | 56 | defp add_interface_macro_flag(compiler_flags, interface) do 57 | macro_flag = "-DBUNDLEX_#{interface |> Atom.to_string() |> String.upcase()}" 58 | [macro_flag] ++ compiler_flags 59 | end 60 | 61 | defp maybe_add_std_flag(compiler_flags, lang) do 62 | if standard_specyfied?(compiler_flags) do 63 | compiler_flags 64 | else 65 | flag = Compilers.get_default_std_flag(lang) 66 | [flag | compiler_flags] 67 | end 68 | end 69 | 70 | defp standard_specyfied?(compiler_flags) do 71 | Enum.any?(compiler_flags, &String.match?(&1, ~r/^-std=/)) 72 | end 73 | 74 | defp link_commands(%Native{type: :lib}, _link, output, objects, _options) do 75 | a_path = path(output <> ".a") 76 | ["rm -f #{a_path}", "ar rcs #{a_path} #{paths(objects)}"] 77 | end 78 | 79 | defp link_commands(native, link, output, objects, options) do 80 | extension = 81 | case native.interface do 82 | :nif -> ".so" 83 | interface when interface in [:cnode, :port] -> "" 84 | end 85 | 86 | wrap_deps = options |> Keyword.get(:wrap_deps, & &1) 87 | 88 | deps = 89 | native.deps 90 | |> Enum.map(&(Toolchain.output_path(&1.app, &1.name, &1.interface) <> ".a")) 91 | |> paths() 92 | |> wrap_deps.() 93 | 94 | linker_flags = native.linker_flags |> Enum.join(" ") 95 | 96 | [ 97 | """ 98 | #{link} -o #{path(output <> extension)} \ 99 | #{deps} #{paths(objects)} #{libs(native)} #{linker_flags} 100 | """ 101 | ] 102 | end 103 | 104 | defp paths(paths, flag \\ "") do 105 | Enum.map_join(paths, " ", fn p -> "#{flag}#{path(p)}" end) 106 | end 107 | 108 | defp path(path) do 109 | path = path |> String.replace(~S("), ~S(\")) |> Path.expand() 110 | ~s("#{path}") 111 | end 112 | 113 | defp libs(native) do 114 | lib_dirs = native.lib_dirs |> paths("-L") 115 | libs = native.libs |> Enum.map_join(" ", fn lib -> "-l#{lib}" end) 116 | 117 | "#{lib_dirs} #{libs}" 118 | end 119 | end 120 | -------------------------------------------------------------------------------- /lib/bundlex/toolchain/common/unix/os_deps.ex: -------------------------------------------------------------------------------- 1 | defmodule Bundlex.Toolchain.Common.Unix.OSDeps do 2 | @moduledoc false 3 | 4 | require Logger 5 | alias Bundlex.Output 6 | 7 | @spec resolve_os_deps(Bundlex.Native.t()) :: Bundlex.Native.t() 8 | def resolve_os_deps(native) do 9 | {cflags_list, libs_list} = 10 | native.os_deps 11 | |> Enum.map(&handle_old_api(native.name, &1)) 12 | |> Enum.map(fn {name, providers} -> 13 | resolve_os_dep(name, native.app, Bunch.listify(providers), native, []) 14 | end) 15 | |> Enum.unzip() 16 | 17 | compiler_flags = cflags_list |> List.flatten() |> Enum.uniq() 18 | libs_flags = libs_list |> List.flatten() |> Enum.uniq() 19 | 20 | %{ 21 | native 22 | | compiler_flags: native.compiler_flags ++ compiler_flags, 23 | linker_flags: native.linker_flags ++ libs_flags 24 | } 25 | end 26 | 27 | defp handle_old_api(native_name, entry) do 28 | is_old_api = 29 | case entry do 30 | {:pkg_config, value} -> 31 | is_binary(value) or (is_list(value) and value != [] and Enum.all?(value, &is_binary/1)) 32 | 33 | {name, _providers} when is_atom(name) -> 34 | false 35 | 36 | {_providers, _lib_names} -> 37 | true 38 | end 39 | 40 | if is_old_api do 41 | Output.warn(""" 42 | Native #{inspect(native_name)} uses deprecated syntax for `os_deps`. \ 43 | See `Bundlex.Project.os_dep` for the new syntax. 44 | """) 45 | 46 | {providers, lib_names} = entry 47 | 48 | name = lib_names |> Bunch.listify() |> Enum.join("_") |> String.to_atom() 49 | 50 | providers = 51 | providers 52 | |> Bunch.listify() 53 | |> Enum.map(fn 54 | {:precompiled, url} -> {:precompiled, url, lib_names} 55 | :pkg_config -> {:pkg_config, lib_names} 56 | end) 57 | 58 | {name, providers} 59 | else 60 | entry 61 | end 62 | end 63 | 64 | defp resolve_os_dep(name, app, [], _native, []) do 65 | Output.raise(""" 66 | Couldn't load OS dependency #{inspect(name)} of package #{app}, \ 67 | because no providers were specified. \ 68 | Make sure to follow installation instructions that may be available in the readme of #{app}. 69 | """) 70 | end 71 | 72 | defp resolve_os_dep(name, app, [], _native, errors) do 73 | Output.raise(""" 74 | Couldn't load OS dependency #{inspect(name)} of package #{app}. \ 75 | Make sure to follow installation instructions that may be available in the readme of #{app}. 76 | 77 | Tried the following providers: 78 | 79 | #{errors |> Enum.reverse() |> Enum.join("\n")} 80 | """) 81 | end 82 | 83 | defp resolve_os_dep(name, app, [provider | providers], native, errors) do 84 | case resolve_os_dep_provider(name, provider, native) do 85 | {:ok, cflags, libs} -> 86 | {cflags, libs} 87 | 88 | {:skip, reason} -> 89 | resolve_os_dep(name, app, providers, native, [ 90 | "Provider `#{inspect(provider)}` #{reason}" | errors 91 | ]) 92 | 93 | {:error, reason} -> 94 | warn_provider_change(reason, provider, providers) 95 | 96 | resolve_os_dep(name, app, providers, native, [ 97 | "Provider `#{inspect(provider)}` #{reason}" | errors 98 | ]) 99 | end 100 | end 101 | 102 | defp resolve_os_dep_provider(name, :pkg_config, native) do 103 | resolve_os_dep_provider(name, {:pkg_config, "#{name}"}, native) 104 | end 105 | 106 | defp resolve_os_dep_provider(_name, {:pkg_config, pkg_configs}, _native) do 107 | pkg_configs = Bunch.listify(pkg_configs) 108 | 109 | with {:ok, cflags} <- get_flags_from_pkg_config(pkg_configs, :cflags), 110 | {:ok, libs} <- get_flags_from_pkg_config(pkg_configs, :libs) do 111 | {:ok, cflags, libs} 112 | end 113 | end 114 | 115 | defp resolve_os_dep_provider(name, {:precompiled, url}, native) do 116 | resolve_os_dep_provider(name, {:precompiled, url, "#{name}"}, native) 117 | end 118 | 119 | defp resolve_os_dep_provider(name, {:precompiled, url, lib_names}, native) do 120 | is_precompiled_disabled = 121 | case Application.get_env(:bundlex, :disable_precompiled_os_deps, false) do 122 | bool when is_boolean(bool) -> bool 123 | [apps: apps] when is_list(apps) -> native.app in apps 124 | end 125 | 126 | if is_precompiled_disabled do 127 | {:skip, 128 | """ 129 | is disabled in the application configuration, check the config.exs file. 130 | """} 131 | else 132 | lib_names = Bunch.listify(lib_names) 133 | 134 | with {:ok, dep_dir_name, dep_path} <- maybe_download_precompiled_package(name, url) do 135 | {:ok, get_precompiled_cflags(dep_path), 136 | get_precompiled_libs_flags(dep_path, dep_dir_name, lib_names, native)} 137 | end 138 | end 139 | end 140 | 141 | defp get_precompiled_libs_flags(dep_path, logical_dep_dir_name, lib_names, native) do 142 | lib_path = Path.join(dep_path, "lib") 143 | logical_output_path = Bundlex.Toolchain.output_path(native.app, native.interface) 144 | create_relative_symlink_or_copy(lib_path, logical_output_path, logical_dep_dir_name) 145 | 146 | # TODO: pass the platform via arguments 147 | # $ORIGIN must be escaped so that it's not treated as an ENV variable 148 | rpath_root = 149 | case Bundlex.get_target() do 150 | %{os: "darwin" <> _rest} -> "@loader_path" 151 | %{os: "linux"} -> "\\$ORIGIN" 152 | %{os: _other} -> "" 153 | end 154 | 155 | [ 156 | "-L#{Path.join(dep_path, "lib")}", 157 | "-Wl,-rpath,#{rpath_root}/#{logical_dep_dir_name}", 158 | "-Wl,-rpath,/opt/homebrew/lib" 159 | ] ++ Enum.map(lib_names, &"-l#{remove_lib_prefix(&1)}") 160 | end 161 | 162 | defp get_precompiled_cflags(dep_path) do 163 | ["-I#{Path.join(dep_path, "include")}"] 164 | end 165 | 166 | defp get_flags_from_pkg_config(pkg_configs, flags_type) do 167 | try do 168 | flags_type = "--#{flags_type}" 169 | System.put_env("PATH", System.get_env("PATH", "") <> ":/usr/local/bin:/opt/homebrew/bin") 170 | 171 | case System.cmd("which", ["pkg-config"]) do 172 | {_path, 0} -> 173 | :ok 174 | 175 | {_path, _error} -> 176 | raise BundlexError, """ 177 | pkg-config not found. Bundlex needs pkg-config to find packages in system. 178 | On Mac OS, you can install pkg-config via Homebrew by typing `brew install pkg-config`. 179 | """ 180 | end 181 | 182 | pkg_configs 183 | |> Enum.map(fn pkg_config -> 184 | case System.cmd("pkg-config", [flags_type, pkg_config], stderr_to_stdout: true) do 185 | {output, 0} -> 186 | String.trim_trailing(output) 187 | 188 | {output, error} -> 189 | raise BundlexError, """ 190 | pkg-config error: 191 | Code: #{error} 192 | #{output} 193 | """ 194 | end 195 | end) 196 | |> then(&{:ok, &1}) 197 | rescue 198 | e -> 199 | error = """ 200 | couldn't load #{inspect(pkg_configs)} libraries with pkg-config due to: 201 | #{format_exception(e)} 202 | """ 203 | 204 | {:error, error} 205 | end 206 | end 207 | 208 | defp remove_lib_prefix("lib" <> libname), do: libname 209 | defp remove_lib_prefix(libname), do: libname 210 | 211 | defp maybe_download_precompiled_package(_name, nil) do 212 | {:error, "ignored, no URL provided"} 213 | end 214 | 215 | defp maybe_download_precompiled_package(name, url) do 216 | precompiled_path = Path.join(Bundlex.Toolchain.bundlex_shared_path(), "precompiled") 217 | File.mkdir_p!(precompiled_path) 218 | package_dir_name = Zarex.sanitize(url) 219 | package_path = Path.join(precompiled_path, package_dir_name) 220 | 221 | if File.exists?(package_path) do 222 | {:ok, package_dir_name, package_path} 223 | else 224 | File.mkdir!(package_path) 225 | 226 | try do 227 | temporary_destination = Path.join(precompiled_path, "temporary") 228 | download(url, temporary_destination) 229 | 230 | {_output, 0} = 231 | System.shell("tar -xf #{temporary_destination} -C #{package_path} --strip-components 1") 232 | 233 | File.rm!(temporary_destination) 234 | {:ok, package_dir_name, package_path} 235 | rescue 236 | e -> 237 | File.rm_rf!(package_path) 238 | 239 | error = """ 240 | couldn't download and extract the precompiled dependency #{inspect(name)} due to: 241 | #{format_exception(e)} 242 | """ 243 | 244 | {:error, error} 245 | end 246 | end 247 | end 248 | 249 | defp download(url, dest) do 250 | response = Req.get!(url) 251 | 252 | case response.status do 253 | 200 -> 254 | File.write!(dest, response.body) 255 | 256 | _other -> 257 | raise BundlexError, """ 258 | Cannot download file from #{url} 259 | Response status: #{response.status} 260 | """ 261 | end 262 | end 263 | 264 | defp format_exception(exception) do 265 | Exception.format(:error, exception) 266 | |> String.trim() 267 | |> String.replace(~r/^/m, "\t") 268 | end 269 | 270 | defp create_relative_symlink_or_copy(target, dir, name) do 271 | link = Path.join(dir, name) 272 | 273 | unless File.exists?(link) do 274 | File.mkdir_p!(dir) 275 | # If the `priv` directory is symlinked by `mix` 276 | # we cannot reliably create a relative symlink 277 | # that would work in all cases, including releases, 278 | # thus we make a copy instead. 279 | {dir_physical, realpath_result} = System.shell("realpath #{dir} 2>/dev/null") 280 | dir_physical = String.trim(dir_physical) 281 | 282 | if realpath_result == 0 and dir_physical == dir do 283 | File.ln_s(path_from_to(dir, target), link) 284 | else 285 | File.mkdir_p!(link) 286 | File.cp_r!(target, link) 287 | end 288 | end 289 | 290 | :ok 291 | end 292 | 293 | defp path_from_to(from, to) do 294 | from = Path.expand(from) |> Path.split() 295 | to = Path.expand(to) |> Path.split() 296 | 297 | longest_common_prefix = 298 | Enum.zip(from, to) 299 | |> Enum.take_while(fn {from, to} -> from == to end) 300 | |> Enum.count() 301 | 302 | Path.join( 303 | Bunch.Enum.repeated("..", Enum.count(from) - longest_common_prefix) ++ 304 | Enum.drop(to, longest_common_prefix) 305 | ) 306 | end 307 | 308 | # pkg_config .pc name can differ on different systems/vendors 309 | # no warning is emited if next provider is same type, example: 310 | # {:pkg_config, "SDL2"}, 311 | # {:pkg_config, "sdl2"} 312 | defp warn_provider_change(reason, provider, [next_provider | _]) do 313 | if provider_type(provider) !== provider_type(next_provider) do 314 | Output.warn(""" 315 | Couldn't load OS dependency using #{inspect(provider)} 316 | 317 | #{reason} 318 | 319 | Loading using #{inspect(next_provider)} 320 | """) 321 | end 322 | end 323 | 324 | defp warn_provider_change(_reason, _provider, _providers), do: nil 325 | 326 | defp provider_type(provider) when is_atom(provider), do: provider 327 | defp provider_type(provider), do: elem(provider, 0) 328 | end 329 | -------------------------------------------------------------------------------- /lib/bundlex/toolchain/custom.ex: -------------------------------------------------------------------------------- 1 | defmodule Bundlex.Toolchain.Custom do 2 | @moduledoc false 3 | 4 | use Bundlex.Toolchain 5 | alias Bundlex.Native 6 | alias Bundlex.Toolchain.Common.Unix 7 | 8 | @impl Toolchain 9 | def compiler_commands(native) do 10 | {compiler, custom_cflags} = 11 | case native.language do 12 | :c -> {System.fetch_env!("CC"), System.fetch_env!("CFLAGS")} 13 | :cpp -> {System.fetch_env!("CXX"), System.fetch_env!("CXXFLAGS")} 14 | end 15 | 16 | {cflags, lflags} = 17 | case native do 18 | %Native{type: :native, interface: :nif} -> 19 | {custom_cflags <> " -fPIC", System.fetch_env!("LDFLAGS") <> " -rdynamic -shared"} 20 | 21 | %Native{type: :lib} -> 22 | {custom_cflags <> " -fPIC", System.fetch_env!("LDFLAGS")} 23 | 24 | %Native{} -> 25 | {custom_cflags, System.fetch_env!("LDFLAGS")} 26 | end 27 | 28 | Unix.compiler_commands( 29 | native, 30 | "#{compiler} #{cflags}", 31 | "#{compiler} #{lflags}", 32 | native.language, 33 | wrap_deps: &"-Wl,--disable-new-dtags,--whole-archive #{&1} -Wl,--no-whole-archive" 34 | ) 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /lib/bundlex/toolchain/gcc.ex: -------------------------------------------------------------------------------- 1 | defmodule Bundlex.Toolchain.GCC do 2 | @moduledoc false 3 | 4 | use Bundlex.Toolchain 5 | alias Bundlex.Native 6 | alias Bundlex.Toolchain.Common.{Compilers, Unix} 7 | 8 | @compilers %Compilers{c: "gcc", cpp: "g++"} 9 | 10 | @impl Toolchain 11 | def compiler_commands(native) do 12 | {cflags, lflags} = 13 | case native do 14 | %Native{type: :native, interface: :nif} -> {"-fPIC", "-rdynamic -shared"} 15 | %Native{type: :lib} -> {"-fPIC", ""} 16 | %Native{} -> {"", ""} 17 | end 18 | 19 | compiler = @compilers |> Map.get(native.language) 20 | 21 | Unix.compiler_commands( 22 | native, 23 | "#{compiler} #{cflags}", 24 | "#{compiler} #{lflags}", 25 | native.language, 26 | wrap_deps: &"-Wl,--disable-new-dtags,--whole-archive #{&1} -Wl,--no-whole-archive" 27 | ) 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /lib/bundlex/toolchain/visual_studio.ex: -------------------------------------------------------------------------------- 1 | defmodule Bundlex.Toolchain.VisualStudio do 2 | @moduledoc false 3 | # Toolchain definition for Microsoft Visual Studio. 4 | # 5 | # It tries to determine Visual Studio root directory before compolation starts 6 | # and set up native.appropriate environment variables that will cause using right 7 | # compiler for given platform by calling vcvarsall.bat script shipped with 8 | # Visual Studio. 9 | # 10 | # Visual Studio directory may be override by setting VISUAL_STUDIO_ROOT 11 | # environment variable. 12 | 13 | use Bundlex.Toolchain 14 | 15 | alias Bundlex.Helper.{GitHelper, PathHelper} 16 | alias Bundlex.Native 17 | alias Bundlex.Output 18 | 19 | @impl true 20 | def before_all!(:windows32) do 21 | [run_vcvarsall("x86")] 22 | end 23 | 24 | @impl true 25 | def before_all!(:windows64) do 26 | [run_vcvarsall("amd64")] 27 | end 28 | 29 | @impl true 30 | def compiler_commands(%Native{interface: interface} = native) do 31 | # TODO escape quotes properly 32 | 33 | includes_part = 34 | Enum.map_join(native.includes, " ", fn include -> 35 | ~s(/I "#{PathHelper.fix_slashes(include)}") 36 | end) 37 | 38 | sources_part = 39 | Enum.map_join(native.sources, " ", fn source -> ~s("#{PathHelper.fix_slashes(source)}") end) 40 | 41 | if not (native.libs |> Enum.empty?()) and not GitHelper.lfs_present?() do 42 | Output.raise( 43 | "Git LFS is not installed, being necessary for downloading windows *.lib files for dlls #{inspect(native.libs)}. Install from https://git-lfs.github.com/." 44 | ) 45 | end 46 | 47 | libs_part = Enum.join(native.libs, " ") 48 | 49 | unquoted_dir_part = 50 | native.app 51 | |> Toolchain.output_path(interface) 52 | |> PathHelper.fix_slashes() 53 | 54 | dir_part = ~s("#{unquoted_dir_part}") 55 | 56 | common_options = "/nologo" 57 | compile_options = "#{common_options} /EHsc /D__WIN32__ /D_WINDOWS /DWIN32 /O2 /c" 58 | link_options = "#{common_options} /INCREMENTAL:NO /FORCE" 59 | 60 | output_path = Toolchain.output_path(native.app, native.name, interface) 61 | 62 | commands = 63 | case native do 64 | %Native{type: :native, interface: :nif} -> 65 | [ 66 | "(cl #{compile_options} #{includes_part} #{sources_part})", 67 | ~s[(link #{link_options} #{libs_part} /DLL /OUT:"#{PathHelper.fix_slashes(output_path)}.dll" *.obj)] 68 | ] 69 | 70 | %Native{type: :lib} -> 71 | [ 72 | "(cl #{compile_options} #{includes_part} #{sources_part})", 73 | ~s[(lib /OUT:"#{PathHelper.fix_slashes(output_path)}.lib" *.obj)] 74 | ] 75 | 76 | %Native{type: type, interface: :nif} when type in [:cnode, :port] -> 77 | [ 78 | "(cl #{compile_options} #{includes_part} #{sources_part})", 79 | ~s[(link /libpath:"#{:code.root_dir() |> Path.join("lib/erl_interface-5.5.2/lib") |> PathHelper.fix_slashes()}" #{link_options} #{libs_part} /OUT:"#{PathHelper.fix_slashes(output_path)}.exe" *.obj)] 80 | ] 81 | end 82 | 83 | [ 84 | "(if exist #{dir_part} rmdir /S /Q #{dir_part})", 85 | "(mkdir #{dir_part})", 86 | "(pushd #{dir_part})", 87 | commands, 88 | "(popd)" 89 | ] 90 | |> List.flatten() 91 | end 92 | 93 | # Runs vcvarsall.bat script 94 | defp run_vcvarsall(vcvarsall_arg) do 95 | program_files = System.fetch_env!("ProgramFiles(x86)") |> Path.expand() 96 | directory_root = Path.join([program_files, "Microsoft Visual Studio"]) 97 | 98 | vcvarsall_path = 99 | directory_root 100 | |> build_vcvarsall_path() 101 | 102 | case File.exists?(vcvarsall_path) do 103 | false -> 104 | Output.raise( 105 | "Unable to find vcvarsall.bat script within Visual Studio root directory. Is your Visual Studio installation valid? (and file is in VC directory?)" 106 | ) 107 | 108 | true -> 109 | ~s/(if not defined VCINSTALLDIR call "#{vcvarsall_path}" #{vcvarsall_arg})/ 110 | end 111 | end 112 | 113 | defp build_vcvarsall_path(root) do 114 | vswhere = Path.join([root, "Installer", "vswhere.exe"]) 115 | vswhere_args = ["-property", "installationPath", "-latest"] 116 | 117 | with true <- File.exists?(vswhere), 118 | {maybe_installation_path, 0} <- System.cmd(vswhere, vswhere_args) do 119 | installation_path = String.trim(maybe_installation_path) 120 | 121 | Path.join([installation_path, "VC", "Auxiliary", "Build", "vcvarsall.bat"]) 122 | |> PathHelper.fix_slashes() 123 | else 124 | false -> 125 | Output.raise( 126 | "Unable to find vswhere.exe at #{vswhere}. Is Visual Studio installed correctly?" 127 | ) 128 | 129 | {_output, return_value} -> 130 | Output.raise( 131 | "vswhere.exe failed with status #{return_value}. Unable to locate Visual Studio installation." 132 | ) 133 | end 134 | end 135 | end 136 | -------------------------------------------------------------------------------- /lib/bundlex/toolchain/xcode.ex: -------------------------------------------------------------------------------- 1 | defmodule Bundlex.Toolchain.XCode do 2 | @moduledoc false 3 | 4 | use Bundlex.Toolchain 5 | 6 | alias Bundlex.Native 7 | alias Bundlex.Toolchain.Common.{Compilers, Unix} 8 | 9 | @compilers %Compilers{c: "cc", cpp: "clang++"} 10 | 11 | @impl Toolchain 12 | def compiler_commands(native) do 13 | {cflags, lflags} = 14 | case native do 15 | %Native{type: :native, interface: :nif} -> 16 | {"-fPIC", "-dynamiclib -undefined dynamic_lookup"} 17 | 18 | %Native{type: :lib} -> 19 | {"-fPIC", ""} 20 | 21 | %Native{} -> 22 | {"", ""} 23 | end 24 | 25 | compiler = @compilers |> Map.get(native.language) 26 | 27 | Unix.compiler_commands( 28 | native, 29 | "#{compiler} #{cflags}", 30 | "#{compiler} #{lflags}", 31 | native.language, 32 | wrap_deps: &"-Wl,-all_load #{&1}" 33 | ) 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /lib/mix/tasks/bundlex.doxygen.ex: -------------------------------------------------------------------------------- 1 | defmodule Mix.Tasks.Bundlex.Doxygen do 2 | @shortdoc "Generates doxygen documentation for Bundlex project" 3 | @moduledoc """ 4 | #{@shortdoc} 5 | 6 | Accepts the following command line arguments: 7 | - `--yes`, `-y` - skips confirmation prompt and overwrites existing meta files 8 | - `--no`, `-n` - skips confirmation prompt and does not overwrite existing meta files 9 | """ 10 | 11 | use Mix.Task 12 | 13 | alias Bundlex.{Doxygen, Output, Project} 14 | alias Bundlex.Helper.MixHelper 15 | 16 | @impl Mix.Task 17 | def run(args) do 18 | {:ok, _apps} = Application.ensure_all_started(:bundlex) 19 | 20 | skip_overwrite_check? = "-y" in args or "--yes" in args 21 | always_overwrite? = "-n" in args or "--no" in args 22 | 23 | if skip_overwrite_check? and always_overwrite? do 24 | Mix.raise("Cannot use both --yes and --no options") 25 | end 26 | 27 | app = MixHelper.get_app!() 28 | 29 | project = get_project(app) 30 | 31 | doxygen = Doxygen.doxygen(project) 32 | 33 | Doxygen.generate_doxyfile(doxygen) 34 | 35 | Doxygen.generate_doxygen_documentation(doxygen) 36 | 37 | if skip_overwrite_check? do 38 | Doxygen.generate_hex_page(doxygen) 39 | else 40 | overwrite? = 41 | always_overwrite? or Mix.shell().yes?("Do you want to overwrite existing hex page?") 42 | 43 | if overwrite? do 44 | Doxygen.generate_hex_page(doxygen) 45 | else 46 | Output.info("Skipping hex page generation") 47 | end 48 | end 49 | 50 | unless page_included?(doxygen.page_path) do 51 | example_docs = """ 52 | defp docs do 53 | [ 54 | extras: [ 55 | "#{doxygen.page_path}", 56 | ... 57 | ], 58 | ... 59 | ] 60 | end 61 | """ 62 | 63 | Output.info(""" 64 | Doxygen documentation page is not included in the project docs. 65 | Add the following snippet to your mix.exs file: 66 | #{example_docs} 67 | """) 68 | end 69 | end 70 | 71 | defp get_project(app) do 72 | with {:ok, project} <- Project.get(app) do 73 | project 74 | else 75 | {:error, reason} -> 76 | Output.raise("Cannot get project for app: #{inspect(app)}, reason: #{inspect(reason)}") 77 | end 78 | end 79 | 80 | defp page_included?(doxygen_page) do 81 | config = Mix.Project.config() 82 | 83 | with {:ok, docs} <- Keyword.fetch(config, :docs), 84 | {:ok, extras} <- Keyword.fetch(docs, :extras) do 85 | doxygen_page in extras 86 | else 87 | :error -> false 88 | end 89 | end 90 | end 91 | -------------------------------------------------------------------------------- /lib/mix/tasks/compile.bundlex.ex: -------------------------------------------------------------------------------- 1 | defmodule Mix.Tasks.Compile.Bundlex do 2 | @shortdoc "Builds natives specified in bundlex.exs file" 3 | @moduledoc """ 4 | #{@shortdoc} 5 | 6 | Accepts the following command line arguments: 7 | - `--store-scripts` - if set, shell scripts are stored in the project 8 | root folder for further analysis. 9 | 10 | Add `:bundlex` to compilers in your Mix project to have this task executed 11 | each time the project is compiled. 12 | """ 13 | use Mix.Task.Compiler 14 | 15 | alias Bundlex.{BuildScript, Native, Output, Platform, Project} 16 | alias Bundlex.Helper.MixHelper 17 | 18 | @recursive true 19 | 20 | @impl true 21 | def run(_args) do 22 | {:ok, _apps} = Application.ensure_all_started(:bundlex) 23 | commands = [] 24 | 25 | app = MixHelper.get_app!() 26 | platform = Platform.get_target!() 27 | 28 | project = 29 | with {:ok, project} <- Project.get(app) do 30 | project 31 | else 32 | {:error, reason} -> 33 | Output.raise("Cannot get project for app: #{inspect(app)}, reason: #{inspect(reason)}") 34 | end 35 | 36 | commands = commands ++ Platform.get_module(platform).toolchain_module().before_all!(platform) 37 | 38 | commands = 39 | commands ++ 40 | case Native.resolve_natives(project, platform) do 41 | {:ok, nifs_commands} -> 42 | nifs_commands 43 | 44 | {:error, {app, reason}} -> 45 | Output.raise( 46 | "Error resolving natives for app #{inspect(app)}, reason: #{inspect(reason)}" 47 | ) 48 | end 49 | 50 | build_script = BuildScript.new(commands) 51 | 52 | {cmdline_options, _argv, _errors} = 53 | OptionParser.parse(System.argv(), switches: [store_scripts: :boolean]) 54 | 55 | if cmdline_options[:store_scripts] do 56 | {:ok, {filename, _script}} = build_script |> BuildScript.store(platform) 57 | Output.info("Stored build script at #{File.cwd!() |> Path.join(filename)}") 58 | end 59 | 60 | case build_script |> BuildScript.run(platform) do 61 | :ok -> 62 | :ok 63 | 64 | {:error, {:run_build_script, return_code: ret, command: cmd}} -> 65 | Output.raise(""" 66 | Failed to build the native part of package #{app}. Errors may have been logged above. 67 | Make sure that all required packages are properly installed in your system. 68 | Requirements and installation guide may be found in the readme of package #{app}. 69 | 70 | Returned code: #{ret} 71 | Build script: 72 | 73 | #{cmd} 74 | """) 75 | 76 | {:error, reason} -> 77 | Output.raise("Error running build script, reason #{inspect(reason)}") 78 | end 79 | 80 | {:ok, []} 81 | end 82 | end 83 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule Bundlex.Mixfile do 2 | use Mix.Project 3 | 4 | @version "1.5.4" 5 | @github_url "https://github.com/membraneframework/bundlex" 6 | 7 | def project do 8 | [ 9 | app: :bundlex, 10 | version: @version, 11 | elixir: "~> 1.12", 12 | elixirc_paths: elixirc_paths(Mix.env()), 13 | start_permanent: Mix.env() == :prod, 14 | deps: deps(), 15 | dialyzer: dialyzer(), 16 | 17 | # hex 18 | description: "Multi-Platform build system for Elixir", 19 | package: package(), 20 | 21 | # docs 22 | name: "Bundlex", 23 | source_url: @github_url, 24 | homepage_url: "https://membraneframework.org", 25 | docs: docs() 26 | ] 27 | end 28 | 29 | def application do 30 | [extra_applications: [:logger], mod: {Bundlex.App, []}] 31 | end 32 | 33 | defp elixirc_paths(:test), do: ["lib", "test/support"] 34 | defp elixirc_paths(_env), do: ["lib"] 35 | 36 | defp package do 37 | [ 38 | maintainers: ["Membrane Team"], 39 | licenses: ["Apache 2.0"], 40 | links: %{ 41 | "GitHub" => @github_url, 42 | "Membrane Framework Homepage" => "https://membraneframework.org" 43 | } 44 | ] 45 | end 46 | 47 | defp docs do 48 | [ 49 | main: "readme", 50 | extras: ["README.md", "LICENSE"], 51 | formatters: ["html"], 52 | source_ref: "v#{@version}" 53 | ] 54 | end 55 | 56 | defp dialyzer() do 57 | opts = [ 58 | flags: [:error_handling], 59 | plt_add_apps: [:mix] 60 | ] 61 | 62 | if System.get_env("CI") == "true" do 63 | # Store PLTs in cacheable directory for CI 64 | [plt_local_path: "priv/plts", plt_core_path: "priv/plts"] ++ opts 65 | else 66 | opts 67 | end 68 | end 69 | 70 | defp deps() do 71 | [ 72 | {:bunch, "~> 1.0"}, 73 | {:qex, "~> 0.5"}, 74 | {:req, ">= 0.4.0"}, 75 | {:elixir_uuid, "~> 1.2"}, 76 | {:zarex, "~> 1.0"}, 77 | {:ex_doc, "~> 0.21", only: :dev, runtime: false}, 78 | {:dialyxir, "~> 1.0", only: :dev, runtime: false}, 79 | {:credo, "~> 1.6", only: :dev, runtime: false} 80 | ] 81 | end 82 | end 83 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{ 2 | "bunch": {:hex, :bunch, "1.6.1", "5393d827a64d5f846092703441ea50e65bc09f37fd8e320878f13e63d410aec7", [:mix], [], "hexpm", "286cc3add551628b30605efbe2fca4e38cc1bea89bcd0a1a7226920b3364fe4a"}, 3 | "bunt": {:hex, :bunt, "1.0.0", "081c2c665f086849e6d57900292b3a161727ab40431219529f13c4ddcf3e7a44", [:mix], [], "hexpm", "dc5f86aa08a5f6fa6b8096f0735c4e76d54ae5c9fa2c143e5a1fc7c1cd9bb6b5"}, 4 | "castore": {:hex, :castore, "1.0.8", "dedcf20ea746694647f883590b82d9e96014057aff1d44d03ec90f36a5c0dc6e", [:mix], [], "hexpm", "0b2b66d2ee742cb1d9cb8c8be3b43c3a70ee8651f37b75a8b982e036752983f1"}, 5 | "credo": {:hex, :credo, "1.7.7", "771445037228f763f9b2afd612b6aa2fd8e28432a95dbbc60d8e03ce71ba4446", [: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", "8bc87496c9aaacdc3f90f01b7b0582467b69b4bd2441fe8aae3109d843cc2f2e"}, 6 | "dialyxir": {:hex, :dialyxir, "1.4.3", "edd0124f358f0b9e95bfe53a9fcf806d615d8f838e2202a9f430d59566b6b53b", [:mix], [{:erlex, ">= 0.2.6", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "bf2cfb75cd5c5006bec30141b131663299c661a864ec7fbbc72dfa557487a986"}, 7 | "earmark_parser": {:hex, :earmark_parser, "1.4.41", "ab34711c9dc6212dda44fcd20ecb87ac3f3fce6f0ca2f28d4a00e4154f8cd599", [:mix], [], "hexpm", "a81a04c7e34b6617c2792e291b5a2e57ab316365c2644ddc553bb9ed863ebefa"}, 8 | "elixir_uuid": {:hex, :elixir_uuid, "1.2.1", "dce506597acb7e6b0daeaff52ff6a9043f5919a4c3315abb4143f0b00378c097", [:mix], [], "hexpm", "f7eba2ea6c3555cea09706492716b0d87397b88946e6380898c2889d68585752"}, 9 | "erlex": {:hex, :erlex, "0.2.7", "810e8725f96ab74d17aac676e748627a07bc87eb950d2b83acd29dc047a30595", [:mix], [], "hexpm", "3ed95f79d1a844c3f6bf0cea61e0d5612a42ce56da9c03f01df538685365efb0"}, 10 | "ex_doc": {:hex, :ex_doc, "0.34.2", "13eedf3844ccdce25cfd837b99bea9ad92c4e511233199440488d217c92571e8", [:mix], [{:earmark_parser, "~> 1.4.39", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "5ce5f16b41208a50106afed3de6a2ed34f4acfd65715b82a0b84b49d995f95c1"}, 11 | "file_system": {:hex, :file_system, "1.0.0", "b689cc7dcee665f774de94b5a832e578bd7963c8e637ef940cd44327db7de2cd", [:mix], [], "hexpm", "6752092d66aec5a10e662aefeed8ddb9531d79db0bc145bb8c40325ca1d8536d"}, 12 | "finch": {:hex, :finch, "0.18.0", "944ac7d34d0bd2ac8998f79f7a811b21d87d911e77a786bc5810adb75632ada4", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: false]}, {:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.3", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 0.2.6 or ~> 1.0", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "69f5045b042e531e53edc2574f15e25e735b522c37e2ddb766e15b979e03aa65"}, 13 | "hpax": {:hex, :hpax, "1.0.0", "28dcf54509fe2152a3d040e4e3df5b265dcb6cb532029ecbacf4ce52caea3fd2", [:mix], [], "hexpm", "7f1314731d711e2ca5fdc7fd361296593fc2542570b3105595bb0bc6d0fad601"}, 14 | "jason": {:hex, :jason, "1.4.3", "d3f984eeb96fe53b85d20e0b049f03e57d075b5acda3ac8d465c969a2536c17b", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "9a90e868927f7c777689baa16d86f4d0e086d968db5c05d917ccff6d443e58a3"}, 15 | "makeup": {:hex, :makeup, "1.1.2", "9ba8837913bdf757787e71c1581c21f9d2455f4dd04cfca785c70bbfff1a76a3", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "cce1566b81fbcbd21eca8ffe808f33b221f9eee2cbc7a1706fc3da9ff18e6cac"}, 16 | "makeup_elixir": {:hex, :makeup_elixir, "0.16.2", "627e84b8e8bf22e60a2579dad15067c755531fea049ae26ef1020cad58fe9578", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "41193978704763f6bbe6cc2758b84909e62984c7752b3784bd3c218bb341706b"}, 17 | "makeup_erlang": {:hex, :makeup_erlang, "1.0.0", "6f0eff9c9c489f26b69b61440bf1b238d95badae49adac77973cbacae87e3c2e", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "ea7a9307de9d1548d2a72d299058d1fd2339e3d398560a0e46c27dab4891e4d2"}, 18 | "mime": {:hex, :mime, "2.0.6", "8f18486773d9b15f95f4f4f1e39b710045fa1de891fada4516559967276e4dc2", [:mix], [], "hexpm", "c9945363a6b26d747389aac3643f8e0e09d30499a138ad64fe8fd1d13d9b153e"}, 19 | "mint": {:hex, :mint, "1.6.2", "af6d97a4051eee4f05b5500671d47c3a67dac7386045d87a904126fd4bbcea2e", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1 or ~> 0.2.0 or ~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "5ee441dffc1892f1ae59127f74afe8fd82fda6587794278d924e4d90ea3d63f9"}, 20 | "nimble_options": {:hex, :nimble_options, "1.1.1", "e3a492d54d85fc3fd7c5baf411d9d2852922f66e69476317787a7b2bb000a61b", [:mix], [], "hexpm", "821b2470ca9442c4b6984882fe9bb0389371b8ddec4d45a9504f00a66f650b44"}, 21 | "nimble_parsec": {:hex, :nimble_parsec, "1.4.0", "51f9b613ea62cfa97b25ccc2c1b4216e81df970acd8e16e8d1bdc58fef21370d", [:mix], [], "hexpm", "9c565862810fb383e9838c1dd2d7d2c437b3d13b267414ba6af33e50d2d1cf28"}, 22 | "nimble_pool": {:hex, :nimble_pool, "1.1.0", "bf9c29fbdcba3564a8b800d1eeb5a3c58f36e1e11d7b7fb2e084a643f645f06b", [:mix], [], "hexpm", "af2e4e6b34197db81f7aad230c1118eac993acc0dae6bc83bac0126d4ae0813a"}, 23 | "qex": {:hex, :qex, "0.5.1", "0d82c0f008551d24fffb99d97f8299afcb8ea9cf99582b770bd004ed5af63fd6", [:mix], [], "hexpm", "935a39fdaf2445834b95951456559e9dc2063d0a055742c558a99987b38d6bab"}, 24 | "req": {:hex, :req, "0.5.2", "70b4976e5fbefe84e5a57fd3eea49d4e9aa0ac015301275490eafeaec380f97f", [:mix], [{:brotli, "~> 0.3.1", [hex: :brotli, repo: "hexpm", optional: true]}, {:ezstd, "~> 1.0", [hex: :ezstd, repo: "hexpm", optional: true]}, {:finch, "~> 0.17", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mime, "~> 2.0.6 or ~> 2.1", [hex: :mime, repo: "hexpm", optional: false]}, {:nimble_csv, "~> 1.0", [hex: :nimble_csv, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "0c63539ab4c2d6ced6114d2684276cef18ac185ee00674ee9af4b1febba1f986"}, 25 | "telemetry": {:hex, :telemetry, "1.2.1", "68fdfe8d8f05a8428483a97d7aab2f268aaff24b49e0f599faa091f1d4e7f61c", [:rebar3], [], "hexpm", "dad9ce9d8effc621708f99eac538ef1cbe05d6a874dd741de2e689c47feafed5"}, 26 | "zarex": {:hex, :zarex, "1.0.5", "58239e3ee5d75f343262bb4df5cf466555a1c689f920e5d3651a9333972f7c7e", [:mix], [], "hexpm", "9fb72ef0567c2b2742f5119a1ba8a24a2fabb21b8d09820aefbf3e592fa9a46a"}, 27 | } 28 | -------------------------------------------------------------------------------- /test/bundlex/integration_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Bundlex.IntegrationTest do 2 | Enum.map([false, true], fn create_priv_dir? -> 3 | module = if create_priv_dir?, do: WithPrivDir, else: WithoutPrivDir 4 | 5 | defmodule module do 6 | use ExUnit.Case 7 | 8 | @tmp "tmp" 9 | 10 | setup_all do 11 | File.rm_rf!(@tmp) 12 | File.cp_r("test_projects", @tmp) 13 | if unquote(create_priv_dir?), do: proj_cmd("mkdir priv") 14 | proj_cmd("mix test") 15 | :ok 16 | end 17 | 18 | test "Generated artifacts are present" do 19 | base_path = "#{@tmp}/example/_build/test/lib/example/priv/bundlex" 20 | 21 | output_files = ["nif/example.so", "cnode/example", "port/example"] 22 | 23 | Enum.each(output_files, fn file -> assert File.exists?("#{base_path}/#{file}") end) 24 | end 25 | 26 | test "Works after changing project directory" do 27 | moved_path = move_proj() 28 | proj_cmd("mix test", project: moved_path, recompile: false) 29 | end 30 | 31 | test "Works in releases" do 32 | proj_cmd("mix release", recompile: false) 33 | File.rename!("#{@tmp}/example/_build/test/rel/example", "#{@tmp}/example_release") 34 | move_proj() 35 | 36 | proj_cmd("bin/example eval \"{7, _v} = Example.Foo.foo(3, 4)\"", 37 | project: "#{@tmp}/example_release" 38 | ) 39 | end 40 | 41 | defp proj_cmd(proj_cmd, opts \\ []) do 42 | {project, opts} = Keyword.pop(opts, :project, "#{@tmp}/example") 43 | {env, opts} = Keyword.pop(opts, :env, []) 44 | {recompile, opts} = Keyword.pop(opts, :recompile, true) 45 | 46 | env = 47 | [ 48 | {"MIX_ENV", "test"}, 49 | {"BUNDLEX_PATH", File.cwd!()}, 50 | {"BUNDLEX_FORCE_NO_COMPILE", unless(recompile, do: "true")} 51 | | env 52 | ] 53 | 54 | family = Bundlex.family() 55 | 56 | {cmd, arg} = 57 | case family do 58 | f when f in [:unix, :custom] -> {"sh", "-c"} 59 | :windows -> {"cmd", "/c"} 60 | end 61 | 62 | assert {_output, 0} = 63 | System.cmd( 64 | cmd, 65 | [arg, "#{proj_cmd} 1>&2"], 66 | [cd: project, env: env] ++ opts 67 | ) 68 | 69 | :ok 70 | end 71 | 72 | # Temporarily moves the project to make sure that 73 | # nothing depends on it being in original location 74 | defp move_proj(project \\ "#{@tmp}/example") do 75 | target_path = "#{project}_moved" 76 | File.rename!(project, target_path) 77 | 78 | on_exit(fn -> 79 | File.rename("#{project}_moved", project) 80 | end) 81 | 82 | target_path 83 | end 84 | end 85 | end) 86 | end 87 | -------------------------------------------------------------------------------- /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | -------------------------------------------------------------------------------- /test_projects/example/.formatter.exs: -------------------------------------------------------------------------------- 1 | # Used by "mix format" 2 | [ 3 | inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] 4 | ] 5 | -------------------------------------------------------------------------------- /test_projects/example/.gitignore: -------------------------------------------------------------------------------- 1 | # The directory Mix will write compiled artifacts to. 2 | /_build/ 3 | 4 | # If you run "mix test --cover", coverage assets end up here. 5 | /cover/ 6 | 7 | # The directory Mix downloads your dependencies sources to. 8 | /deps/ 9 | 10 | # Where third-party dependencies like ExDoc output generated docs. 11 | /doc/ 12 | 13 | # Ignore .fetch files in case you like to edit your project deps locally. 14 | /.fetch 15 | 16 | # If the VM crashes, it generates a dump, let's ignore it too. 17 | erl_crash.dump 18 | 19 | # Also ignore archive artifacts (built via "mix archive.build"). 20 | *.ez 21 | 22 | # Ignore package tarball (built via "mix hex.build"). 23 | native-*.tar 24 | 25 | -------------------------------------------------------------------------------- /test_projects/example/bundlex.exs: -------------------------------------------------------------------------------- 1 | defmodule Example.BundlexProject do 2 | use Bundlex.Project 3 | 4 | def project do 5 | [ 6 | natives: natives() 7 | ] 8 | end 9 | 10 | defp get_ffmpeg() do 11 | url = 12 | case Bundlex.get_target() do 13 | %{abi: "musl"} -> 14 | nil 15 | 16 | %{architecture: "aarch64", os: "linux"} -> 17 | "https://github.com/BtbN/FFmpeg-Builds/releases/download/autobuild-2023-11-30-12-55/ffmpeg-n6.0.1-linuxarm64-gpl-shared-6.0.tar.xz" 18 | 19 | %{architecture: "x86_64", os: "linux"} -> 20 | "https://github.com/BtbN/FFmpeg-Builds/releases/download/autobuild-2023-11-30-12-55/ffmpeg-n6.0.1-linux64-gpl-shared-6.0.tar.xz" 21 | 22 | %{architecture: "x86_64", os: "darwin" <> _rest_of_os_name} -> 23 | "https://github.com/membraneframework-precompiled/precompiled_ffmpeg/releases/latest/download/ffmpeg_macos_intel.tar.gz" 24 | 25 | %{architecture: "aarch64", os: "darwin" <> _rest_of_os_name} -> 26 | "https://github.com/membraneframework-precompiled/precompiled_ffmpeg/releases/latest/download/ffmpeg_macos_arm.tar.gz" 27 | 28 | _other -> 29 | nil 30 | end 31 | 32 | 33 | [{:precompiled, url, ["libswscale", "libavcodec"]}] 34 | end 35 | 36 | defp natives do 37 | [ 38 | example: [ 39 | deps: [example_lib: :example_lib], 40 | src_base: "example", 41 | sources: ["foo_nif.c"], 42 | interface: [:nif], 43 | os_deps: [ 44 | {:pkg_config, "libpng"}, # deprecated syntax, testing for regression 45 | ffmpeg: get_ffmpeg(), 46 | ] 47 | ], 48 | example: [ 49 | deps: [example_lib: :example_lib], 50 | src_base: "example", 51 | sources: ["example_cnode.c"], 52 | interface: [:cnode] 53 | ], 54 | example: [ 55 | src_base: "example", 56 | sources: ["example_port.c"], 57 | interface: :port 58 | ] 59 | ] 60 | end 61 | end 62 | -------------------------------------------------------------------------------- /test_projects/example/c_src/example/example_cnode.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #ifndef _REENTRANT 10 | #define _REENTRANT // For some reason __erl_errno is undefined unless _REENTRANT 11 | // is defined 12 | #endif 13 | #include 14 | #include 15 | #include 16 | 17 | double foo(double a, double b) { 18 | return add(a, b); 19 | } 20 | 21 | int listen_sock(int *listen_fd, int *port) { 22 | int fd = socket(AF_INET, SOCK_STREAM, 0); 23 | assert(fd > 0); 24 | 25 | int opt_on = 1; 26 | assert(setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &opt_on, sizeof(opt_on)) == 0); 27 | 28 | struct sockaddr_in addr; 29 | unsigned int addr_size = sizeof(addr); 30 | addr.sin_family = AF_INET; 31 | addr.sin_port = htons(0); 32 | addr.sin_addr.s_addr = htonl(INADDR_ANY); 33 | 34 | assert(bind(fd, (struct sockaddr *)&addr, addr_size) == 0); 35 | assert(getsockname(fd, (struct sockaddr *)&addr, &addr_size) == 0); 36 | *port = (int)ntohs(addr.sin_port); 37 | const int queue_size = 5; 38 | assert(listen(fd, queue_size) == 0); 39 | 40 | *listen_fd = fd; 41 | return 0; 42 | } 43 | 44 | int handle_message(int ei_fd, char *node_name, erlang_msg emsg, 45 | ei_x_buff *in_buf) { 46 | ei_x_buff out_buf; 47 | ei_x_new_with_version(&out_buf); 48 | int decode_idx = 0; 49 | int version; 50 | char fun[255]; 51 | int arity; 52 | 53 | assert(ei_decode_version(in_buf->buff, &decode_idx, &version) == 0); 54 | ei_decode_tuple_header(in_buf->buff, &decode_idx, &arity); 55 | assert(ei_decode_atom(in_buf->buff, &decode_idx, fun) == 0); 56 | 57 | double res = 0.0; 58 | if (!strcmp(fun, "foo")) { 59 | double a, b; 60 | assert(ei_decode_double(in_buf->buff, &decode_idx, &a) == 0); 61 | assert(ei_decode_double(in_buf->buff, &decode_idx, &b) == 0); 62 | res = foo(a, b); 63 | 64 | assert(ei_x_encode_tuple_header(&out_buf, 2) == 0); 65 | assert(ei_x_encode_atom(&out_buf, node_name) == 0); 66 | assert(ei_x_encode_double(&out_buf, res) == 0); 67 | ei_send(ei_fd, &emsg.from, out_buf.buff, out_buf.index); 68 | } 69 | 70 | ei_x_free(&out_buf); 71 | return 0; 72 | } 73 | 74 | int receive(int ei_fd, char *node_name) { 75 | ei_x_buff in_buf; 76 | ei_x_new(&in_buf); 77 | erlang_msg emsg; 78 | int res = 0; 79 | switch (ei_xreceive_msg_tmo(ei_fd, &emsg, &in_buf, 5000)) { 80 | case ERL_TICK: 81 | break; 82 | case ERL_ERROR: 83 | if (erl_errno == ETIMEDOUT) { 84 | fprintf(stderr, "Timeout. Message not received."); 85 | } 86 | res = erl_errno; 87 | break; 88 | default: 89 | if (emsg.msgtype == ERL_REG_SEND && 90 | handle_message(ei_fd, node_name, emsg, &in_buf)) { 91 | res = -1; 92 | } 93 | break; 94 | } 95 | 96 | ei_x_free(&in_buf); 97 | return res; 98 | } 99 | 100 | int validate_args(int argc, char **argv) { 101 | assert(argc == 5); 102 | for (int i = 1; i < argc; i++) { 103 | assert(strlen(argv[i]) < 255); 104 | } 105 | return 0; 106 | } 107 | 108 | 109 | int main(int argc, char **argv) { 110 | assert(validate_args(argc, argv) == 0); 111 | 112 | char host_name[256]; 113 | strcpy(host_name, argv[1]); 114 | char alive_name[256]; 115 | strcpy(alive_name, argv[2]); 116 | char node_name[256]; 117 | strcpy(node_name, argv[3]); 118 | short creation = (short)atoi(argv[4]); 119 | char *cookie = getenv("BUNDLEX_ERLANG_COOKIE"); 120 | 121 | int listen_fd; 122 | int port; 123 | assert(listen_sock(&listen_fd, &port) == 0); 124 | 125 | ei_cnode ec; 126 | struct in_addr addr; 127 | addr.s_addr = inet_addr("127.0.0.1"); 128 | assert(ei_connect_xinit(&ec, host_name, alive_name, node_name, &addr, cookie, 129 | creation) >= 0); 130 | assert(ei_publish(&ec, port) != -1); 131 | printf("ready\r\n"); 132 | fflush(stdout); 133 | 134 | ErlConnect conn; 135 | int ei_fd = ei_accept_tmo(&ec, listen_fd, &conn, 5000); 136 | assert(ei_fd != ERL_ERROR); 137 | 138 | int res = receive(ei_fd, node_name); 139 | 140 | close(listen_fd); 141 | close(ei_fd); 142 | return res; 143 | } 144 | -------------------------------------------------------------------------------- /test_projects/example/c_src/example/example_port.c: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | int main() { 4 | fprintf(stdout, "bundlex_port_test"); 5 | } 6 | -------------------------------------------------------------------------------- /test_projects/example/c_src/example/foo_nif.c: -------------------------------------------------------------------------------- 1 | /** @file */ 2 | 3 | #include 4 | #include 5 | 6 | #include 7 | 8 | typedef struct State { 9 | struct SwsContext *sws_context; 10 | int width, height; 11 | enum AVPixelFormat src_format, dst_format; 12 | 13 | uint8_t *src_data[4], *dst_data[4]; 14 | int src_linesize[4], dst_linesize[4]; 15 | 16 | int dst_image_size; 17 | } State; 18 | 19 | /** 20 | * @brief NIF function to call the C function from the example_lib library, 21 | * that adds two integers. 22 | * 23 | * @param argc Unused. 24 | * @param argv Only the first element is used. 25 | * @return ERL_NIF_TERM 26 | */ 27 | static ERL_NIF_TERM export_foo(ErlNifEnv *env, int argc, 28 | const ERL_NIF_TERM argv[]) { 29 | (void)argc; 30 | int a, b; 31 | enif_get_int(env, argv[0], &a); 32 | enif_get_int(env, argv[1], &b); 33 | int v = swscale_version(); 34 | return enif_make_tuple2(env, enif_make_int(env, add(a, b)), 35 | enif_make_int(env, v)); 36 | } 37 | 38 | static ErlNifFunc nif_funcs[] = {{"foo", 2, export_foo, 0}}; 39 | 40 | ERL_NIF_INIT(Elixir.Example.Foo.Nif, nif_funcs, NULL, NULL, NULL, NULL) 41 | -------------------------------------------------------------------------------- /test_projects/example/lib/example/foo.ex: -------------------------------------------------------------------------------- 1 | defmodule Example.Foo do 2 | use Bundlex.Loader, nif: :example 3 | 4 | defnif(foo(a, b)) 5 | end 6 | -------------------------------------------------------------------------------- /test_projects/example/mix.exs: -------------------------------------------------------------------------------- 1 | defmodule Example.MixProject do 2 | use Mix.Project 3 | 4 | @bundlex_path System.fetch_env!("BUNDLEX_PATH") 5 | 6 | def project do 7 | [ 8 | app: :example, 9 | version: "0.1.0", 10 | elixir: "~> 1.10", 11 | compilers: if(System.get_env("BUNDLEX_FORCE_NO_COMPILE"), do: [], else: [:bundlex]) ++ Mix.compilers(), 12 | start_permanent: Mix.env() == :prod, 13 | deps_path: "#{@bundlex_path}/deps", 14 | lockfile: "#{@bundlex_path}/mix.lock", 15 | deps: deps(), 16 | docs: docs() 17 | ] 18 | end 19 | 20 | # Run "mix help compile.app" to learn about applications. 21 | def application do 22 | [ 23 | extra_applications: [:logger] 24 | ] 25 | end 26 | 27 | # Run "mix help deps" to learn about dependencies. 28 | defp deps do 29 | [ 30 | {:bundlex, path: @bundlex_path}, 31 | {:example_lib, path: "../example_lib"}, 32 | {:ex_doc, "~> 0.21", only: :dev, runtime: false} 33 | ] 34 | end 35 | 36 | defp docs do 37 | [ 38 | extras: [ 39 | "pages/doxygen/example.md" 40 | ] 41 | ] 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /test_projects/example/pages/doxygen/example.md: -------------------------------------------------------------------------------- 1 | # Native code documentation 2 | [Doxygen documentation of the native code](./doxygen/example/html/index.html) 3 | -------------------------------------------------------------------------------- /test_projects/example/test/example_test.exs: -------------------------------------------------------------------------------- 1 | defmodule ExampleTest do 2 | use ExUnit.Case 3 | 4 | test "native with interface NIF" do 5 | assert {11, v} = Example.Foo.foo(5, 6) 6 | assert is_integer(v) 7 | end 8 | 9 | # test "native with interface CNode" do 10 | # test_cnode(:example) 11 | # end 12 | 13 | test "native with interface port" do 14 | test_port(:example) 15 | end 16 | 17 | test "timeout mechanism" do 18 | require Bundlex.CNode 19 | assert {:ok, cnode} = Bundlex.CNode.start_link(:example) 20 | 21 | assert_raise RuntimeError, ~r/Timeout upon call to the CNode*/, fn -> 22 | Bundlex.CNode.call(cnode, :non_existing_func, 1000) 23 | end 24 | end 25 | 26 | defp test_cnode(name) do 27 | require Bundlex.CNode 28 | assert {:ok, cnode} = Bundlex.CNode.start_link(name) 29 | assert 10.0 = Bundlex.CNode.call(cnode, {:foo, 5.0, 5.0}) 30 | end 31 | 32 | defp test_port(name) do 33 | require Bundlex.Port 34 | _port = Bundlex.Port.open(name) 35 | 36 | receive do 37 | {_port, {:data, msg}} -> assert msg == ~c"bundlex_port_test" 38 | after 39 | 2_000 -> raise "timeout" 40 | end 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /test_projects/example/test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | -------------------------------------------------------------------------------- /test_projects/example_lib/.formatter.exs: -------------------------------------------------------------------------------- 1 | # Used by "mix format" 2 | [ 3 | inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] 4 | ] 5 | -------------------------------------------------------------------------------- /test_projects/example_lib/.gitignore: -------------------------------------------------------------------------------- 1 | # The directory Mix will write compiled artifacts to. 2 | /_build/ 3 | 4 | # If you run "mix test --cover", coverage assets end up here. 5 | /cover/ 6 | 7 | # The directory Mix downloads your dependencies sources to. 8 | /deps/ 9 | 10 | # Where third-party dependencies like ExDoc output generated docs. 11 | /doc/ 12 | 13 | # Ignore .fetch files in case you like to edit your project deps locally. 14 | /.fetch 15 | 16 | # If the VM crashes, it generates a dump, let's ignore it too. 17 | erl_crash.dump 18 | 19 | # Also ignore archive artifacts (built via "mix archive.build"). 20 | *.ez 21 | 22 | # Ignore package tarball (built via "mix hex.build"). 23 | native-*.tar 24 | 25 | -------------------------------------------------------------------------------- /test_projects/example_lib/bundlex.exs: -------------------------------------------------------------------------------- 1 | defmodule Example.Lib.BundlexProject do 2 | use Bundlex.Project 3 | 4 | def project do 5 | [ 6 | libs: libs(), 7 | ] 8 | end 9 | 10 | defp libs do 11 | [ 12 | example_lib: [ 13 | src_base: "example_lib", 14 | sources: ["example_lib_nif.c"], 15 | interface: :nif 16 | ], 17 | example_lib: [ 18 | src_base: "example_lib", 19 | sources: ["example_lib_cnode.c"], 20 | interface: :cnode 21 | ], 22 | ] 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /test_projects/example_lib/c_src/example_lib/example_lib_cnode.c: -------------------------------------------------------------------------------- 1 | #include "example_lib_cnode.h" 2 | 3 | double add(double a, double b) { 4 | // some operations that require erl_interface.h 5 | ei_x_buff buf; 6 | ei_x_new(&buf); 7 | ei_x_free(&buf); 8 | return a + b; 9 | } 10 | -------------------------------------------------------------------------------- /test_projects/example_lib/c_src/example_lib/example_lib_cnode.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | double add(double a, double b); 7 | -------------------------------------------------------------------------------- /test_projects/example_lib/c_src/example_lib/example_lib_nif.c: -------------------------------------------------------------------------------- 1 | #include "example_lib_nif.h" 2 | 3 | int add(int a, int b) { 4 | some_nif_op(); 5 | return a + b; 6 | } 7 | 8 | int sub(int a, int b) { 9 | some_nif_op(); 10 | return a - b; 11 | } 12 | 13 | void some_nif_op() { 14 | // some operations that require erl_nif.h 15 | int *some_int_ptr = enif_alloc(sizeof(int)); 16 | enif_free(some_int_ptr); 17 | } 18 | -------------------------------------------------------------------------------- /test_projects/example_lib/c_src/example_lib/example_lib_nif.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | int add(int a, int b); 7 | int sub(int a, int b); 8 | void some_nif_op(void); 9 | -------------------------------------------------------------------------------- /test_projects/example_lib/mix.exs: -------------------------------------------------------------------------------- 1 | defmodule Example.Lib.MixProject do 2 | use Mix.Project 3 | 4 | @bundlex_path System.fetch_env!("BUNDLEX_PATH") 5 | 6 | def project do 7 | [ 8 | app: :example_lib, 9 | version: "0.1.0", 10 | elixir: "~> 1.10", 11 | compilers: if(System.get_env("BUNDLEX_FORCE_NO_COMPILE"), do: [], else: [:bundlex]) ++ Mix.compilers(), 12 | start_permanent: Mix.env() == :prod, 13 | deps_path: "#{@bundlex_path}/deps", 14 | lockfile: "#{@bundlex_path}/mix.lock", 15 | deps: deps() 16 | ] 17 | end 18 | 19 | # Run "mix help compile.app" to learn about applications. 20 | def application do 21 | [ 22 | extra_applications: [:logger] 23 | ] 24 | end 25 | 26 | # Run "mix help deps" to learn about dependencies. 27 | defp deps do 28 | [ 29 | {:bundlex, path: @bundlex_path, override: true} 30 | ] 31 | end 32 | end 33 | --------------------------------------------------------------------------------