PageRenderTime 46ms CodeModel.GetById 16ms RepoModel.GetById 1ms app.codeStats 0ms

/lib/mix/lib/mix/tasks/compile.erlang.ex

https://github.com/vanstee/elixir
Elixir | 289 lines | 232 code | 46 blank | 11 comment | 24 complexity | 21e37d80be0ea079329b00212c8d7eff MD5 | raw file
Possible License(s): Apache-2.0
  1. defmodule Mix.Tasks.Compile.Erlang do
  2. alias :epp, as: Epp
  3. alias :digraph, as: Graph
  4. alias :digraph_utils, as: GraphUtils
  5. use Mix.Task
  6. @hidden true
  7. @shortdoc "Compile Erlang source files"
  8. @recursive true
  9. @manifest ".compile.erlang"
  10. @moduledoc """
  11. A task to compile Erlang source files.
  12. When this task runs, it will first check the modification times of
  13. all files to be compiled and if they haven't been
  14. changed since the last compilation, it will not compile
  15. them. If any of them have changed, it compiles
  16. everything.
  17. For this reason, the task touches your `:compile_path`
  18. directory and sets the modification time to the current
  19. time and date at the end of each compilation. You can
  20. force compilation regardless of modification times by passing
  21. the `--force` option.
  22. ## Command line options
  23. * `--force` - forces compilation regardless of modification times
  24. ## Configuration
  25. * `ERL_COMPILER_OPTIONS` - can be used to give default compile options.
  26. The value must be a valid Erlang term. If the value is a list, it will
  27. be used as is. If it is not a list, it will be put into a list.
  28. * `:erlc_paths` - directories to find source files.
  29. Defaults to `["src"]`, can be configured as:
  30. ```
  31. [erlc_paths: ["src", "other"]]
  32. ```
  33. * `:erlc_include_path` - directory for adding include files.
  34. Defaults to `"include"`, can be configured as:
  35. ```
  36. [erlc_include_path: "other"]
  37. ```
  38. * `:erlc_options` - compilation options that apply to Erlang's
  39. compiler. `:debug_info` is enabled by default.
  40. There are many available options here:
  41. http://www.erlang.org/doc/man/compile.html#file-2
  42. """
  43. defrecord Erl, file: nil, module: nil, behaviours: [], compile: [],
  44. includes: [], mtime: nil, invalid: false
  45. @doc """
  46. Runs this task.
  47. """
  48. def run(args) do
  49. { opts, _, _ } = OptionParser.parse(args, switches: [force: :boolean])
  50. project = Mix.project
  51. source_paths = project[:erlc_paths]
  52. include_path = to_erl_file project[:erlc_include_path]
  53. compile_path = to_erl_file Mix.Project.compile_path(project)
  54. files = Mix.Utils.extract_files(source_paths, [:erl])
  55. erlc_options = project[:erlc_options] || []
  56. erlc_options = erlc_options ++ [{:outdir, compile_path}, {:i, include_path}, :report]
  57. erlc_options = Enum.map erlc_options, fn
  58. { kind, dir } when kind in [:i, :outdit] ->
  59. { kind, to_erl_file(dir) }
  60. opt ->
  61. opt
  62. end
  63. tuples = files
  64. |> scan_sources(include_path, source_paths)
  65. |> sort_dependencies
  66. |> Enum.map(&annotate_target(&1, compile_path, opts[:force]))
  67. compile_mappings(manifest(), tuples, fn
  68. input, _output ->
  69. file = to_erl_file(Path.rootname(input, ".erl"))
  70. :compile.file(file, erlc_options)
  71. end)
  72. end
  73. @doc """
  74. Returns Erlang manifests.
  75. """
  76. def manifests, do: [manifest]
  77. defp manifest, do: Path.join(Mix.Project.manifest_path, @manifest)
  78. @doc """
  79. Extracts the extensions from the mappings, automatically
  80. invoking the callback for each stale input and output pair
  81. (or for all if `force` is true) and removing files that no
  82. longer have a source, while keeping the manifest up
  83. to date.
  84. ## Examples
  85. For example, a simple compiler for Lisp Flavored Erlang
  86. would be implemented like:
  87. compile_mappings ".compile.lfe",
  88. [{ "src", "ebin" }],
  89. :lfe, :beam, opts[:force], fn
  90. input, output ->
  91. :lfe_comp.file(to_erl_file(input),
  92. [output_dir: Path.dirname(output)])
  93. end
  94. The command above will:
  95. 1. Look for files ending with the `lfe` extension in `src`
  96. and their `beam` counterpart in `ebin`;
  97. 2. For each stale file (or for all if `force` is true),
  98. invoke the callback passing the calculated input
  99. and output;
  100. 3. Update the manifest with the newly compiled outputs;
  101. 4. Remove any output in the manifest that that does not
  102. have an equivalent source;
  103. The callback must return `{ :ok, mod }` or `:error` in case
  104. of error. An error is raised at the end if any of the
  105. files failed to compile.
  106. """
  107. def compile_mappings(manifest, mappings, src_ext, dest_ext, force, callback) do
  108. files = lc { src, dest } inlist mappings do
  109. extract_targets(src, src_ext, dest, dest_ext, force)
  110. end |> Enum.concat
  111. compile_mappings(manifest, files, callback)
  112. end
  113. @doc """
  114. Converts the given file to a format accepted by
  115. the Erlang compilation tools.
  116. """
  117. def to_erl_file(file) do
  118. to_char_list(file)
  119. end
  120. ## Internal helpers
  121. defp scan_sources(files, include_path, source_paths) do
  122. include_paths = [include_path | source_paths]
  123. Enum.reduce(files, [], &scan_source(&2, &1, include_paths)) |> Enum.reverse
  124. end
  125. defp scan_source(acc, file, include_paths) do
  126. erl_file = Erl[file: file, module: module_from_artifact(file)]
  127. case Epp.parse_file(to_erl_file(file), include_paths, []) do
  128. { :ok, forms } ->
  129. [List.foldl(tl(forms), erl_file, &do_form(file, &1, &2)) | acc]
  130. { :error, _error } ->
  131. acc
  132. end
  133. end
  134. defp do_form(file, form, Erl[] = erl) do
  135. case form do
  136. {:attribute, _, :file, {include_file, _}} when file != include_file ->
  137. if File.regular?(include_file) do
  138. erl.update_includes &[include_file|&1]
  139. else
  140. erl
  141. end
  142. {:attribute, _, :behaviour, behaviour} ->
  143. erl.update_behaviours &[behaviour|&1]
  144. {:attribute, _, :compile, value} ->
  145. erl.update_compile &[value|&1]
  146. _ ->
  147. erl
  148. end
  149. end
  150. defp sort_dependencies(erls) do
  151. graph = Graph.new
  152. lc erl inlist erls do
  153. Graph.add_vertex(graph, erl.module, erl)
  154. end
  155. lc erl inlist erls do
  156. lc b inlist erl.behaviours, do: Graph.add_edge(graph, b, erl.module)
  157. lc c inlist erl.compile do
  158. case c do
  159. {:parse_transform, transform} -> Graph.add_edge(graph, transform, erl.module)
  160. _ -> :ok
  161. end
  162. end
  163. end
  164. result =
  165. case GraphUtils.topsort(graph) do
  166. false -> erls
  167. mods ->
  168. lc m inlist mods, do: elem(Graph.vertex(graph, m), 1)
  169. end
  170. Graph.delete(graph)
  171. result
  172. end
  173. defp annotate_target(erl, compile_path, force) do
  174. beam = Path.join(compile_path, "#{erl.module}#{:code.objfile_extension}")
  175. if force || Mix.Utils.stale?([erl.file|erl.includes], [beam]) do
  176. { erl.file, erl.module, beam }
  177. else
  178. { erl.file, erl.module, nil }
  179. end
  180. end
  181. defp module_from_artifact(artifact) do
  182. artifact |> Path.basename |> Path.rootname
  183. end
  184. defp extract_targets(dir1, src_ext, dir2, dest_ext, force) do
  185. files = Mix.Utils.extract_files([dir1], List.wrap(src_ext))
  186. lc file inlist files do
  187. module = module_from_artifact(file)
  188. target = Path.join(dir2, module <> "." <> to_string(dest_ext))
  189. if force || Mix.Utils.stale?([file], [target]) do
  190. { file, module, target }
  191. else
  192. { file, module, nil }
  193. end
  194. end
  195. end
  196. defp compile_mappings(manifest, tuples, callback) do
  197. # Stale files are the ones with a destination
  198. stale = lc { src, _mod, dest } inlist tuples, dest != nil, do: { src, dest }
  199. # Get the previous entries from the manifest
  200. entries = Mix.Utils.read_manifest(manifest)
  201. # Files to remove are the ones in the
  202. # manifest but they no longer have a source
  203. removed = Enum.filter(entries, fn entry ->
  204. module = module_from_artifact(entry)
  205. not Enum.any?(tuples, fn { _src, mod, _dest } -> module == mod end)
  206. end)
  207. if stale == [] && removed == [] do
  208. :noop
  209. else
  210. # Build the project structure so we can write down compiled files.
  211. Mix.Project.build_structure
  212. # Remove manifest entries with no source
  213. Enum.each(removed, &File.rm/1)
  214. # Compile stale files and print the results
  215. results = lc { input, output } inlist stale do
  216. interpret_result(input, callback.(input, output))
  217. end
  218. # Write final entries to manifest
  219. entries = (entries -- removed) ++ Enum.map(stale, &elem(&1, 1))
  220. Mix.Utils.write_manifest(manifest, :lists.usort(entries))
  221. # Raise if any error, return :ok otherwise
  222. if :error in results, do: raise CompileError
  223. :ok
  224. end
  225. end
  226. defp interpret_result(file, result) do
  227. case result do
  228. { :ok, _ } -> Mix.shell.info "Compiled #{file}"
  229. :error -> :error
  230. end
  231. result
  232. end
  233. end