Packages
gpb
4.1.0
5.0.0
4.21.7
4.21.6
4.21.5
4.21.4
4.21.3
4.21.2
4.21.1
4.21.0
4.20.0
4.19.9
4.19.8
4.19.7
4.19.6
4.19.5
4.19.4
4.19.3
4.19.2
4.19.1
4.19.0
4.18.0
4.17.7
4.17.6
4.17.5
4.17.3
4.17.2
4.17.1
4.17.0
4.16.2
4.16.1
4.16.0
4.15.2
4.15.1
4.14.2
4.14.1
4.14.0
4.13.0
4.12.0
4.11.2
4.11.1
4.11.0
4.10.6
4.10.5
4.10.4
4.10.3
4.10.2
4.10.1
4.10.0
4.9.3
4.9.2
4.9.1
4.9.0
4.8.0
4.7.3
4.7.2
4.7.1
4.7.0
4.6.0
4.5.1
4.5.0
4.4.1
4.4.0
4.3.3
4.3.2
4.3.1
4.3.0
4.2.3
4.2.2
4.2.1
4.2.0
4.1.9
4.1.8
4.1.7
4.1.6
4.1.5
4.1.4
4.1.3
4.1.2
4.1.1
4.1.0
4.0.2
4.0.1
4.0.0
3.28.1
3.28.0
3.27.7
3.27.6
3.27.5
3.27.4
3.27.3
3.27.2
3.27.1
3.27.0
3.26.8
3.26.7
3.26.6
3.26.5
3.26.4
3.26.3
3.26.2
3.26.1
3.26.0
3.25.2
3.25.1
3.25.0
3.24.4
3.24.3
3.24.2
3.24.1
3.24.0
3.23.2
3.23.1
3.23.0
3.22.5
3.22.4
3.22.3
3.22.2
3.22.1
3.22.0
3.21.3
3.21.2
3.21.1
3.21.0
3.20.3
3.20.2
3.20.0
3.19.0
3.18.10
3.18.9
3.18.8
3.18.7
3.18.6
3.18.5
3.18.4
3.18.3
3.18.2
3.18.1
3.18.0
3.17.13
3.17.12
3.17.11
3.17.10
3.17.9
3.17.8
3.17.5
3.17.4
3.17.3
3.17.2
3.17.1
3.17.0
3.16.0
3.15.0
3.14.0
3.13.0
3.12.2
3.12.1
3.12.0
3.11.0
A compiler for Google protocol buffer definitions files for Erlang.
Current section
Files
Jump to
Current section
Files
src/gpb_compile.erl
%%% Copyright (C) 2010-2013 Tomas Abrahamsson
%%%
%%% Author: Tomas Abrahamsson <tab@lysator.liu.se>
%%%
%%% This library is free software; you can redistribute it and/or
%%% modify it under the terms of the GNU Lesser General Public
%%% License as published by the Free Software Foundation; either
%%% version 2.1 of the License, or (at your option) any later version.
%%%
%%% This library is distributed in the hope that it will be useful,
%%% but WITHOUT ANY WARRANTY; without even the implied warranty of
%%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
%%% Lesser General Public License for more details.
%%%
%%% You should have received a copy of the GNU Lesser General Public
%%% License along with this library; if not, write to the Free Software
%%% Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
%%% MA 02110-1301 USA
-module(gpb_compile).
%-compile(export_all).
-export([file/1, file/2]).
-export([string/2, string/3]).
-export([proto_defs/2, proto_defs/3]).
-export([msg_defs/2, msg_defs/3]).
-export([format_error/1, format_warning/1]).
-export([c/0, c/1, c/2]). % Cmd line interface, halts vm---don't use from shell!
-export([parse_opts_and_args/1]).
-export([show_args/0]).
-export([show_version/0]).
-export([locate_import/2]).
-export([read_import/2]).
-include_lib("kernel/include/file.hrl").
-include_lib("eunit/include/eunit.hrl").
-include("../include/gpb.hrl").
-include("gpb_codegen.hrl").
-include("gpb_compile.hrl").
-import(gpb_lib, [replace_term/2]).
%% -- Types -----------------------------------------------------
%% Options
-type boolean_opt(X) :: X | {X, boolean()}.% Just an option `X' means `{X,true}'
-type directory() :: string().
-type opts() :: [opt()].
-type opt() :: type_specs | {type_specs, boolean() | preferably} |
{verify, optionally | always | never} |
{copy_bytes, true | false | auto | integer() | float()} |
{strings_as_binaries, boolean()} | strings_as_binaries |
boolean_opt(defs_as_proplists) |
boolean_opt(descriptor) |
boolean_opt(maps) |
boolean_opt(msgs_as_maps) |
boolean_opt(mapfields_as_maps) |
boolean_opt(defs_as_maps) |
{maps_unset_optional, omitted | present_undefined} |
boolean_opt(nif) |
{load_nif, string()} |
{i, directory()} |
{o, directory()} |
{o_erl, directory()} | {o_hrl, directory()} |
{o_nif_cc, directory()} |
binary | to_proto_defs | to_msg_defs |
return |
boolean_opt(return_warnings) | boolean_opt(return_errors) |
report |
boolean_opt(report_warnings) | boolean_opt(report_errors) |
boolean_opt(warnings_as_errors) |
boolean_opt(include_as_lib) |
boolean_opt(use_packages) |
{erlc_compile_options,string()} |
{rename, renaming()} |
{msg_name_prefix,
string() | atom() |
{by_proto, prefix_by_proto()}} |
{msg_name_suffix, string() | atom()} |
boolean_opt(msg_name_to_snake_case) |
boolean_opt(msg_name_to_lower) |
{module_name_prefix, string() | atom()} |
{module_name_suffix, string() | atom()} |
{module_name, string() | atom()} |
{any_translate, [translation()]} |
boolean_opt(epb_compatibility) |
boolean_opt(epb_functions) |
boolean_opt(defaults_for_omitted_optionals) |
boolean_opt(type_defaults_for_omitted_optionals) |
{import_fetcher, import_fetcher_fun()} |
{target_erlang_version, integer() | current} |
term().
-type renaming() :: {pkg_name, name_change()} |
{msg_name, msg_name_change()} |
{msg_fqname, msg_name_change()} |
{group_name, name_change()} |
{group_fqname, name_change()} |
{service_name, name_change()} |
{service_fqname, name_change()} |
{rpc_name, name_change()}.
-type name_change() :: {prefix, string() | atom()} |
{suffix, string() | atom()} |
lowercase |
snake_case |
dots_to_underscores |
base_name.
-type msg_name_change() :: name_change() |
{prefix, {by_proto, prefix_by_proto()}}.
-type prefix_by_proto() :: [{ProtoName::atom(), Prefix::string() | atom()}].
-type translation() :: {encode, mod_fn_argtemplate()} |
{decode, mod_fn_argtemplate()} |
{merge, mod_fn_argtemplate()} |
{verify, mod_fn_argtemplate()}.
-type fn_name() :: atom().
-type mod_fn_argtemplate() :: {module(), fn_name(), arg_template()}.
-type arg_template() :: [arg()].
-type arg() :: term() | named_arg().
-type named_arg() :: '$1' | '$2' | '$errorf' | '$user_data' | '$op'.
-type fetcher_ret() :: from_file | {ok, string()} | {error, term()}.
-type import_fetcher_fun() :: fun((string()) -> fetcher_ret()).
%% Compilation return values
-type comp_ret() :: mod_ret() | bin_ret() | error_ret().
-type mod_ret() :: ok | {ok, [warning()]}.
-type bin_ret() :: {ok, module(), code()} |
{ok, module(), code(), [warning()]}.
-type error_ret() :: error | {error, reason()} | {error, reason(), [warning()]}.
-type warning() :: term().
-type reason() :: term().
-type code() :: binary() | gpb_parse:defs() | [code_item()].
-type code_item() :: {erl, ErlCode :: binary()} |
{nif, NifCcText :: string()}.
-export_type([opts/0, opt/0]).
-export_type([comp_ret/0]).
%% @equiv file(File, [])
-spec file(string()) -> comp_ret().
file(File) ->
file(File, []).
%% @doc
%% Compile a .proto file to a .erl file and to a .hrl file.
%%
%% The `File' argument must not include path to the .proto file. Example:
%% "SomeDefinitions.proto" is ok, while "/path/to/SomeDefinitions.proto"
%% is not ok.
%%
%% The .proto file is expected to be found in a directories specified by an
%% `{i,directory()}' option. It is possible to specify `{i,directory()}'
%% several times, they will be searched in the order specified.
%%
%% The `type_specs' option enables or disables `::Type()' annotations
%% in the generated .hrl file. Default is `true' if there are no
%% cyclic message dependencies. The default changed in gpb version 4.0.0.
%% Previously, the default was `false'.
%% If you have messages referencing other messages cyclically, and get into
%% troubles when compiling the generated files, set this to `false'.
%%
%% The `verify' option specifies whether or not to generate code
%% that verifies, during encoding, that values are of correct type and
%% within range. The `verify' option can have the following values:
%% <dl>
%% <dt>`always'</dt><dd>Generate code that unconditionally
%% verifies values.</dd>
%% <dt>`never'</dt><dd>Generate code that never verifies
%% values time. Encoding will fail if a value of the wrong
%% type is supplied. This includes forgetting to set a required
%% message field. Encoding may silently truncate values out of
%% range for some types.</dd>
%% <dt>`optionally'</dt><dd>Generate an `encode_msg/2' that accepts
%% the run-time option `verify' or `{verify,boolean()}' for specifying
%% whether or not to verify values.</dd>
%% </dl>
%%
%% Erlang value verification either succeeds or crashes with the `error'
%% `{gpb_type_error,Reason}'. Regardless of the `verify' option,
%% a function, `verify_msg/1' is always generated.
%%
%% The `copy_bytes' option specifies whether when decoding data of
%% type `bytes' (or strings if the `strings_as_binaries' is set), the
%% decoded bytes should be copied or not. Copying requires the
%% `binary' module, which first appeared in Erlang R14A. When not
%% copying decoded bytes, they will become sub binaries of the larger
%% input message binary. This may tie up the memory in the input
%% message binary longer than necessary after it has been
%% decoded. Copying the decoded bytes will avoid creating sub
%% binaries, which will in turn make it possible to free the input message
%% binary earlier. The `copy_bytes' option can have the following values:
%% <dl>
%% <dt>`false'</dt><dd>Never copy bytes/(sub-)binaries.</dd>
%% <dt>`true'</dt><dd>Always copy bytes/(sub-)binaries.</dd>
%% <dt>`auto'</dt><dd>Copy bytes/(sub-)binaries if the beam vm,
%% on which the compiler (this module) is running,
%% has the `binary:copy/1' function. (This is the default)</dd>
%% <dt>integer() | float()</dt><dd>Copy the bytes/(sub-)binaries if the
%% message this many times or more larger than the size of the
%% bytes/(sub-)binary.</dd>
%% </dl>
%%
%% The `strings_as_binaries' option specifies whether strings should
%% be returned from decoding as strings (list of Unicode code points),
%% or as binaries (UTF-8 encoded). The `copy_bytes' option applies
%% to strings as well, when the `strings_as_binaries' option is set.
%% Upon encoding, both binaries and iolists are accepted.
%%
%% The `defs_as_proplists' option changes the generated introspection
%% functions `find_msg_def' and `get_msg_defs' to return the description
%% of each message field as a proplist, instead of as a `#field{}' record.
%% The purpose is to make the generated code completely independent
%% of gpb, at compile-time (it is already independent at run-time).
%% The keys of the proplist are the names of the record fields in the
%% `#field{}' record. See also {@link gpb:proplists_to_field_records()}
%% and related functions for conversion functions between these two
%% formats.
%%
%% The `descriptor' option specifies whether or not to generate a
%% function, descriptor/0, which returns a binary that describes the
%% proto file(s) contents according to the protobuf's `descriptor.proto'.
%% The default is to not generate such a description. The generated
%% description binary is most likely not identical to what `protoc'
%% would generate, but the contents is roughly equivalent.
%%
%% The `{o,directory()}' option specifies directory to use for storing
%% the generated `.erl' and `.hrl' files. Default is the same
%% directory as for the proto `File'.
%%
%% The `{o_erl,directory()}', `{o_hrl,directory()}', `{o_nif_cc,directory()}',
%% options specify output directories for where to generate the `.erl'
%% and `.hrl' files respectively, and for the NIF C++ file,
%% if the `nif' option is specified. The `{o_erl,directory()}' option
%% overrides any `{o,directory()}' option, and similarly for the
%% other file-type specific output options.
%%
%% The `maps' option will generate a protobuf encoder/decoder that
%% uses maps instead of records. This option expands to the following
%% options:
%% <dl>
%% <dt>`msgs_as_maps'</dt>
%% <dd>No `.hrl' file will be generated, and the functions
%% `encode_msg', `merge_msgs' and `verify_msg' will take the
%% message name as an additional parameter.</dd>
%% <dt>`mapfields_as_maps'</dt>
%% <dd>The value for fields of type `map<_,_>' will be a map
%% instead of a list of 2-tuples.</dd>
%% <dt>`defs_as_maps'</dt>
%% <dd>The introspection will generate message field descriptions
%% as maps instead of as `#field{}' records, unless, of course
%% `defs_as_proplists' is specified, in which case they will be
%% proplists instead.</dd>
%% </dl>
%%
%% For messages as maps, for optional fields, if not set, the
%% `maps_unset_optional' option specifies the Erlang-internal
%% representation; both how it is expected to be found at encoding,
%% and how decoding will return it:
%% <dl>
%% <dt>`omitted'</dt>
%% <dd>This means it is not included in the map.
%% This is the default. (since gpb version 4.0.0)
%% </dd>
%% <dt>`present_undefined'</dt>
%% <dd>This means it is present and has the value `undefined'.
%% This <em>was</em> the default before gpb version 4.0.0.
%% </dd>
%% </dl>
%%
%% The `nif' option will cause the compiler to generate nif C++ code
%% for encoding and decoding. The generated nif C++ code can be linked
%% with the Google protobuf C++ library. Read the file
%% `README.nif-cc' for more info. This option is not compatible with
%% the `maps' option; the generated C++ decoding code would still
%% create records.
%%
%% The `binary' option will cause the generated and compiled code to be
%% returned as a binary. No files will be written. The return value
%% will be on the form `{ok,Mod,Code}' or `{ok,Mod,Code,Warnings}'
%% if the compilation is successful. This option may be useful
%% e.g. when generating test cases. In case the `nif' option is set,
%% the `Code' will be a list of tuples: `{erl,binary()}' which
%% contains the Erlang object byte code, and `{nif,binary()}' which
%% contains the C++ code. You will have to compile the C++ code with a
%% C++ compiler, before you can use the Erlang code.
%%
%% The `to_proto_defs' option will result in `{ok,Defs}' or
%% `{ok,Defs,Warns}' being returned if the compilation is successful.
%% The returned message definitions can be used with the
%% {@link proto_defs/2} or {@link proto_defs/3} functions.
%%
%% The `to_msg_defs' option is a deprecated alias for `to_proto_defs'.
%%
%% <dl>
%% <dt>`report_errors'/`report_warnings'</dt>
%% <dd>Causes errors/warnings to be printed as they occur.</dd>
%% <dt>`report'</dt>
%% <dd>This is a short form for both `report_errors' and
%% `report_warnings'.</dd>
%% <dt>`return_errors'</dt>
%% <dd>If this flag is set, then `{error,ErrorList,WarningList}' is
%% returned when there are errors.</dd>
%% <dt>`return_warnings'</dt>
%% <dd>If this flag is set, then an extra field containing `WarningList'
%% is added to the tuples returned on success.</dd>
%% <dt>`return'</dt>
%% <dd>This is a short form for both `return_errors' and
%% `return_warnings'.</dd>
%% </dl>
%%
%% Setting the `warnings_as_errors' option will cause warnings to be
%% treated as errors. If there are warnings but no errors, and
%% `return_warnings' is not specified, then `error' will be returned.
%%
%% See {@link format_error/1} for a way to turn an error <i>Reason</i> to
%% plain text.
%%
%% If the `include_as_lib' option is set, the generated code will include
%% gpb.hrl as a library, which is necessary if dependencies are managed with
%% Rebar. Otherwise, the header file is included directly and must be located
%% in the path, which is default behavior.
%%
%% The `use_packages' option instructs gpb to prepend the name of a package
%% to every message it contains. If no package is defined, nothing will be
%% prepended. This enables the reference of messages in other packages which
%% would otherwise not be possible. However, for reasons of backward
%% compatibility, this option is disabled by default.
%%
%% If the the `{erlc_compile_options,string()}' option is set,
%% then the genereted code will contain a directive `-compile([String]).'
%%
%% The `{rename,{What,How}}' can transform message names, package names,
%% service and rpc names in various ways. This option supersedes the
%% options `{msg_name_prefix,Prefix}', `{msg_name_suffix,Suffix}',
%% `msg_name_to_lower' and `msg_name_to_snake_case', while at the same
%% time giving more fine-grained control. It is for example possible to
%% apply snake_casing only to the message name, while keeping the
%% package name, the service name and the rpc name intact. This can be
%% useful with grpc, where these name components are exposed. The
%% `msg_fqname' refers to the fully qualified message name, as in
%% `Package.MsgName', while the `msg_name' refers to just the message
%% name without package. The `service_fqname' and `service_name' specifiers
%% work analogously.
%%
%% It is possible to stack `rename' options, and they will be applied in
%% the order they are specified. So it is for example possible to
%% snake_case a name, and then also prefix it.
%%
%% The `{msg_name_prefix,Prefix}' will add `Prefix' (a string or an atom)
%% to each message. This might be useful for resolving colliding names,
%% when incorporating several protocol buffer definitions into the same
%% project. The `{msg_name_suffix,Suffix}' works correspondingly.
%%
%% The `{msg_name_prefix,Prefix}' option expands
%% to `[{rename,{pkg_name,Prefix}},{rename,{msg_fqname,{prefix,Prefix}}},
%% {rename,{group_fqname,{prefix,Prefix}}}}]',
%% and ditto for suffixes.
%%
%% For backwards compatibility, the `{msg_name_prefix,{by_proto,PrefixList}}'
%% expands to just `[{rename,{msg_fqname,{prefix,PrefixList}}}]'.
%%
%% The `msg_name_to_lower' and `msg_name_to_snake_case' options expands
%% to `[{rename,{pkg_name,X}},{rename,{service_fqname,X}},
%% {rename,{rpc_name,X}},{rename,{msg_fqname,X}},
%% {rename,{rpc_name,X}},{rename,{group_fqname,X}}]' where `X' is
%% `lowercase' or `snake_case' respectively.
%%
%% The `{module_name_prefix,Prefix}' will add `Prefix' (a string or an atom)
%% to the generated code and definition files. The `{module_name_suffix,Suffix}'
%% works correspondingly. For the case of compatibility with Erlang Protobuffs,
%% the `epb_compatibility' option implies `{module_name_suffix,"_pb"}'
%%
%% The `{module_name,Name}' can be used to specify the module name of the
%% generated code freely, instead of basing it on the proto file name.
%% The name specified with `module_name' can be prefixed and suffixed with
%% the `module_name_prefix' and `module_name_suffix' options.
%% The `any_translate' option can be used to provide packer and
%% unpacker functions for `google.protobuf.Any' messages. The merge
%% translator is optional, and is called either via the `merge_msgs'
%% function in the generated code, or when the decoder sees another
%% `Any' message. The default merge operation is to let the second
%% element overwrite previous elements. The verify translator is
%% optional too, since verification can be disabled.
%% The translation calls are specified as `{Mod,Fn,ArgTemplate}' where
%% `Mod',`Fn' is a module and function to call, `ArgTemplate' is a list
%% of terms, containing markers, such as `$1', `$2' and so on, for where
%% to place the actual args. This makes it possible to specify additional
%% static argument terms, for instance.
%% The translator functions are called as follows:
%% <dl>
%% <dt>Encode (Packing)</dt>
%% <dd>Call `Mod:Fn(Term)' to pack the `Term' (`$1') to
%% a `google.protobuf.Any' message.</dd>
%% <dt>Decode (Unpacking)</dt>
%% <dd>Call `Mod:Fn(Any)' to unpack the `Any' (`$1') to
%% unpack a `google.protobuf.Any' message to a term.</dd>
%% <dt>Merge </dt>
%% <dd>Call `Mod:Fn(Term1, Term2) -> Term3' to merge two
%% unpacked terms to a resulting Term3. The `$1' is the
%% previously seen term (during decoding, on encountering a
%% second `Any' field), or the first argument to the
%% `merge_msgs' function. The `$2' is the lastly seen term, or
%% the second argument to the `merge_msgs' function.</dd>
%% <dt>Verify</dt>
%% <dd>Call `Mod:Fn(Term) -> _' to verify an unpacked `Term'.
%% If `Term' (`$1') is valid, the function is expected to just return
%% any value, which is ignored and discarded.
%% If `Term' is invalid, the function is exptected to not
%% return anything, but instead either crash, call
%% `erlang:error/1', or `throw/1' or `exit/1'. with the
%% reason for error.
%% (For backwards compatibility, it is also possible
%% to have an error function as argument, using `$errorf',
%% but this is deprecated.)</dd>
%% </dl>
%% There are additional translator argument markers:
%% <dl>
%% <dt>`$user_data'</dt>
%% <dd>This will be replaced by the `user_data' option to the
%% generated `encode_msg', `decode_msg', `merge_msgs' and
%% `verify_msg' functions. If that option is not specified, the
%% value `undefined' is used substituted for `$user_data'.</dd>
%% <dt>`$op'</dt>
%% <dd>This will be replaced by `encode', `decode', `merge' or
%% `verify', depending on from which context it is actually
%% called. This can be useful because if the message is to be
%% verified on encoding (see the `verify' option), then the same
%% options, and thus the same user-data, are used for both
%% `encode_msg' and for `verify_msg'. The `$op' marker makes it
%% possible to tell these two call sites apart, if needed.</dd>
%% </dl>
%%
%% The `epb_compatibility' option is an umbrella-option for
%% compatibility with the Erlang protobuffs library. It will expand to
%% the options below. It will expand in-place, meaning any of these
%% can be overridden if specified before the `epb_compatibility'
%% option.
%% <ul>
%% <li>`epb_functions'</li>
%% <li>`defaults_for_omitted_optionals'</li>
%% <li>`{module_name_suffix,"_pb"}'</li>
%% <li>`{msg_name_to_lower,true}'</li>
%% </ul>
%%
%% If the `epb_functions' option is specified, then for compatibility
%% with Erlang protobuffs, the following functions will be generated:
%% <ul>
%% <li>`encode/1'</li>
%% <li>`encode_<MsgName>/1'</li>
%% <li>`decode/2'</li>
%% <li>`decode_<MsgName>/1'</li>
%% </ul>
%%
%% The `defaults_for_omitted_optionals' and
%% `type_defaults_for_omitted_optionals' options generates code that
%% set default values or type-defaults respectively, on decoding, if
%% an optional field is not present in the binary to decode. Normally
%% it would otherwise have been set to `undefined'. Note that with
%% these options it is not possible to determine after decoding
%% whether a field contained data in the binary message. Also note
%% that these options are only applicable for proto2 syntax messages,
%% and are ignored for proto3 syntax messages. (For proto3, it
%% effectively <em>must</em> be ignored, since, on the wire, a field
%% set to its type-default value is indistinguishable from an omitted
%% value.)
%%
%% The `import_fetcher' option can be used to catch imports. The
%% option value must be a function taking one argument, the name of
%% the file to import. It must return either `from_file', letting this
%% file pass through the normal file import, or `{ok,string()}' if it
%% has fetched the file itself, or `{error,term()}'.
%%
%% The `target_erlang_version` can be used to specify another major
%% version of Erlang/OTP to generate code for. The default, `current'
%% means that the generated code is expected to be compiled and run
%% on the same major version as gpb runs on.
-spec file(string(), opts()) -> comp_ret().
file(File, Opts) ->
do_file_or_string(File, Opts).
%% @equiv string(Mod, Str, [])
-spec string(module(), string()) -> comp_ret().
string(Mod, Str) ->
string(Mod, Str, []).
%% @doc
%% Compile a `.proto' file as string. See {@link file/2} for information
%% on options and return values.
-spec string(module(), string(), opts()) -> comp_ret().
string(Mod, Str, Opts) ->
do_file_or_string({Mod, Str}, Opts).
do_file_or_string(In, Opts0) ->
Opts1 = normalize_opts(Opts0),
case parse_file_or_string(In, Opts1) of
{ok, Defs} ->
case gpb_names:rename_defs(Defs, Opts1) of
{ok, Defs1} ->
Mod = find_out_mod(In, Opts1),
DefaultOutDir = find_default_out_dir(In),
Opts2 = Opts1 ++ [{o,DefaultOutDir}],
do_proto_defs_aux1(Mod, Defs1, Opts2);
{error, Reason} = Error ->
possibly_report_error(Error, Opts1),
case proplists:get_bool(return_warnings, Opts1) of
true -> {error, Reason, []};
false -> Error
end
end;
{error, Reason} = Error ->
possibly_report_error(Error, Opts1),
case proplists:get_bool(return_warnings, Opts1) of
true -> {error, Reason, []};
false -> Error
end
end.
normalize_opts(Opts0) ->
normalize_return_report_opts(
normalize_alias_opts(Opts0)).
normalize_alias_opts(Opts) ->
lists:foldl(fun(F, OptsAcc) -> F(OptsAcc) end,
Opts,
[fun norm_opt_alias_to_msg_proto_defs/1,
fun norm_opt_epb_compat_opt/1,
fun norm_opt_map_opts/1]).
norm_opt_alias_to_msg_proto_defs(Opts) ->
lists:map(fun(to_msg_defs) -> to_proto_defs;
({to_msg_defs, Bool}) -> {to_proto_defs, Bool};
(Opt) -> Opt
end,
Opts).
norm_opt_epb_compat_opt(Opts) ->
proplists:expand(
[{epb_compatibility, [epb_functions,
defaults_for_omitted_optionals,
{module_name_suffix,"_pb"},
{msg_name_to_lower, true}]},
{{epb_compatibility,false}, [{epb_functions,false},
{defaults_for_omitted_optionals,false}]}],
Opts).
norm_opt_map_opts(Opts) ->
proplists:expand(
[{maps, [msgs_as_maps,
mapfields_as_maps,
defs_as_maps]},
{{maps,false}, [{msgs_as_maps, false},
{mapfields_as_maps, false},
{defs_as_maps, false}]}],
Opts).
normalize_return_report_opts(Opts1) ->
Opts2 = expand_opt(return, [return_warnings, return_errors], Opts1),
Opts3 = expand_opt(report, [report_warnings, report_errors], Opts2),
Opts4 = unless_defined_set(return_warnings, report_warnings, Opts3),
Opts5 = unless_defined_set(return_errors, report_errors, Opts4),
Opts5.
expand_opt(OptionToTestFor, OptionsToExpandTo, Opts) ->
lists:append(
lists:map(fun(Opt) when Opt == OptionToTestFor -> OptionsToExpandTo;
(Opt) -> [Opt]
end,
Opts)).
unless_defined_set(OptionToTestFor, Default, Opts) ->
case is_option_defined(OptionToTestFor, Opts) of
true -> Opts;
false -> Opts ++ [Default]
end.
is_option_defined(Key, Opts) ->
lists:any(fun({K, _V}) -> K =:= Key;
(K) -> K =:= Key
end,
Opts).
find_out_mod({Mod, _S}, _Opts) ->
Mod;
find_out_mod(File, Opts) ->
gpb_names:file_name_to_module_name(File, Opts).
find_default_out_dir({_Mod, _S}) -> ".";
find_default_out_dir(File) -> filename:dirname(File).
%% @equiv proto_defs(Mod, Defs, [])
-spec proto_defs(module(), gpb_parse:defs()) -> comp_ret().
proto_defs(Mod, Defs) ->
proto_defs(Mod, Defs, []).
%% @doc
%% Compile a list of pre-parsed definitions to file or to a binary.
%% See {@link file/2} for information on options and return values.
-spec proto_defs(module(), gpb_parse:defs(), opts()) -> comp_ret().
proto_defs(Mod, Defs, Opts) ->
do_proto_defs_aux1(Mod, Defs, normalize_opts(Opts)).
do_proto_defs_aux1(Mod, Defs0, Opts0) ->
{IsAcyclic, Defs} = try_topsort_defs(Defs0),
possibly_probe_defs(Defs, Opts0),
Warns0 = check_unpackables_marked_as_packed(Defs),
{Warns1, Opts1} = possibly_adjust_typespec_opt(IsAcyclic, Opts0),
Warns = Warns0 ++ Warns1,
AnRes = gpb_analyzer:analyze_defs(Defs, Opts1),
case verify_opts(Defs, Opts1) of
ok ->
Res1 = do_proto_defs_aux2(Defs, clean_module_name(Mod), AnRes,
Opts1),
return_or_report_warnings_or_errors(Res1, Warns, Opts1,
get_output_format(Opts1));
{error, OptError} ->
return_or_report_warnings_or_errors({error, OptError}, [], Opts1,
get_output_format(Opts1))
end.
verify_opts(Defs, Opts) ->
while_ok([fun() -> verify_opts_any_translate_and_nif(Opts) end,
fun() -> verify_opts_epb_compat(Defs, Opts) end]).
while_ok(Funs) ->
lists:foldl(fun(F, ok) -> F();
(_, Err) -> Err
end,
ok,
Funs).
verify_opts_any_translate_and_nif(Opts) ->
case {proplists:get_value(any_translate, Opts),
proplists:get_bool(nif, Opts)} of
{Translations, true} when Translations /= undefined ->
{error, {invalid_options,any_translate,nif}};
_ ->
ok
end.
verify_opts_epb_compat(Defs, Opts) ->
while_ok(
[fun() ->
case {proplists:get_bool(epb_functions, Opts),
gpb_lib:get_records_or_maps_by_opts(Opts)} of
{true, maps} ->
{error, {invalid_options, epb_functions,maps}};
_ ->
ok
end
end,
fun() ->
case proplists:get_bool(epb_functions, Opts) of
true ->
case lists:member(msg, gpb_lib:msg_names(Defs)) of
true ->
{error, {epb_functions_impossible,
{with_msg_named,msg}}};
false ->
ok
end;
false ->
ok
end
end]).
%% @equiv msg_defs(Mod, Defs, [])
%% @doc Deprecated, use proto_defs/2 instead.
-spec msg_defs(module(), gpb_parse:defs()) -> comp_ret().
msg_defs(Mod, Defs) ->
msg_defs(Mod, Defs, []).
%% @spec msg_defs(Mod, Defs, Opts) -> CompRet
%% @equiv proto_defs(Mod, Defs, Opts)
%% @doc Deprecated, use proto_defs/2 instead.
-spec msg_defs(module(), gpb_parse:defs(), opts()) -> comp_ret().
msg_defs(Mod, Defs, Opts) ->
proto_defs(Mod, Defs, Opts).
do_proto_defs_aux2(Defs, Mod, AnRes, Opts) ->
case get_output_format(Opts) of
proto_defs ->
{ok, Defs};
binary ->
ErlTxt = format_erl(Mod, Defs, AnRes, Opts),
HrlTxt = possibly_format_hrl(Mod, Defs, Opts),
NifTxt = possibly_format_nif_cc(Mod, Defs, AnRes, Opts),
compile_to_binary(Mod, HrlTxt, ErlTxt, NifTxt, Opts);
file ->
ErlTxt = format_erl(Mod, Defs, AnRes, Opts),
HrlTxt = possibly_format_hrl(Mod, Defs, Opts),
NifTxt = possibly_format_nif_cc(Mod, Defs, AnRes, Opts),
ErlOutDir = get_erl_outdir(Opts),
HrlOutDir = get_hrl_outdir(Opts),
NifCcOutDir = get_nif_cc_outdir(Opts),
Erl = filename:join(ErlOutDir, atom_to_list(Mod) ++ ".erl"),
Hrl = filename:join(HrlOutDir, atom_to_list(Mod) ++ ".hrl"),
NifCc = filename:join(NifCcOutDir, atom_to_list(Mod) ++ ".nif.cc"),
case {file_write_file(Erl, ErlTxt, Opts),
possibly_write_file(Hrl, HrlTxt, Opts),
possibly_write_file(NifCc, NifTxt, Opts)} of
{ok, ok, ok} -> ok;
{{error, R}, _, _} -> {error, {write_failed, Erl, R}};
{_, {error, R}, _} -> {error, {write_failed, Erl, R}};
{_, _, {error, R}} -> {error, {write_failed, NifCc, R}}
end
end.
return_or_report_warnings_or_errors(Res, ExtraWarns, Opts, OutFormat) ->
Res2 = merge_warns(Res, ExtraWarns, OutFormat),
possibly_report_warnings(Res2, Opts),
possibly_report_error(Res2, Opts),
return_warnings_or_errors(Res2, Opts).
merge_warns(ok, Warns, _OutFmt) -> {ok, Warns};
merge_warns({ok, Warns1}, Warns2, file) -> {ok, Warns2++Warns1};
merge_warns({ok, Defs}, Warns, proto_defs) -> {ok, Defs, Warns};
merge_warns({ok, M, B}, Warns, binary) -> {ok, M, B, Warns};
merge_warns({ok, M, B, Warns1}, Warns2, binary) -> {ok, M, B, Warns2++Warns1};
merge_warns({error, R}, Warns, _OutFmt) -> {error, R, Warns};
merge_warns({error, R, Warns1}, Warns2, _OutFmt) -> {error, R, Warns2++Warns1};
merge_warns(error, Warns, binary) ->
erlang:error({internal_error, ?MODULE,
generated_code_failed_to_compile, Warns}).
possibly_report_warnings(Result, Opts) ->
Warns = case Result of
{error, _Reason, Ws} -> Ws;
{ok, _M, _B, Ws} -> Ws;
{ok, _Defs, Ws} -> Ws;
{ok, Ws} -> Ws
end,
case proplists:get_bool(report_warnings, Opts) of
true -> lists:foreach(fun report_warning/1, Warns);
false -> ok
end.
report_warning(Warn) ->
io:format("~s~n", [format_warning(Warn)]).
possibly_report_error(Res, Opts) ->
case {Res, proplists:get_bool(report_errors, Opts)} of
{{error, _Reason, _Warns}, true} ->
io:format("~s~n", [format_error(Res)]);
{{error, _Reason}, true} ->
io:format("~s~n", [format_error(Res)]);
_ ->
ok
end.
return_warnings_or_errors(Res, Opts) ->
case proplists:get_bool(return_warnings, Opts) of
true ->
case proplists:get_bool(warnings_as_errors, Opts) of
true -> turn_warnings_to_errors_keep(Res);
false -> Res
end;
false ->
case proplists:get_bool(warnings_as_errors, Opts) of
true -> turn_warnings_to_errors_remove(Res);
false -> remove_warnings_from_res(Res)
end
end.
turn_warnings_to_errors_keep({ok, _Mod, _Bin, []}=Res) -> Res;
turn_warnings_to_errors_keep({ok, _MsgDefs, []}=Res) -> Res;
turn_warnings_to_errors_keep({ok, []}=Res) -> Res;
turn_warnings_to_errors_keep({ok, _Mod, _Bin, Warns}) -> {error, [], Warns};
turn_warnings_to_errors_keep({ok, _MsgDefs, Warns}) -> {error, [], Warns};
turn_warnings_to_errors_keep({ok, Warns}) -> {error, [], Warns};
turn_warnings_to_errors_keep({error, R, Warns}) -> {error, R, Warns}.
turn_warnings_to_errors_remove({ok, Mod, Bin, []}) -> {ok, Mod, Bin};
turn_warnings_to_errors_remove({ok, MsgDefs, []}) -> {ok, MsgDefs};
turn_warnings_to_errors_remove({ok, []}) -> ok;
turn_warnings_to_errors_remove({ok, _Mod, _Bin, _Warns}) -> error;
turn_warnings_to_errors_remove({ok, _MsgDefs, _Warns}) -> error;
turn_warnings_to_errors_remove({ok, _Warns}) -> error;
turn_warnings_to_errors_remove({error, R, _Warns}) -> {error, R}.
remove_warnings_from_res({ok, Mod, Bin, _Warns}) -> {ok, Mod, Bin};
remove_warnings_from_res({ok, MsgDefs, _Warns}) -> {ok, MsgDefs};
remove_warnings_from_res({ok, _Warns}) -> ok;
remove_warnings_from_res({error, R, _Warns}) -> {error, R}.
get_output_format([binary | _]) -> binary;
get_output_format([{binary, true} | _]) -> binary;
get_output_format([to_proto_defs | _]) -> proto_defs;
get_output_format([{to_proto_defs, true} | _]) -> proto_defs;
get_output_format([_ | Rest]) -> get_output_format(Rest);
get_output_format([]) -> file.
get_erl_outdir(Opts) ->
proplists:get_value(o_erl, Opts, get_outdir(Opts)).
get_hrl_outdir(Opts) ->
proplists:get_value(o_hrl, Opts, get_outdir(Opts)).
get_nif_cc_outdir(Opts) ->
proplists:get_value(o_nif_cc, Opts, get_outdir(Opts)).
get_outdir(Opts) ->
proplists:get_value(o, Opts, ".").
clean_module_name(Mod) ->
Clean = re:replace(atom_to_list(Mod), "[.]", "_", [global, {return,list}]),
list_to_atom(Clean).
%% @spec format_error({error, Reason} | Reason) -> io_list()
%% Reason = term()
%%
%% @doc Produce a plain-text error message from a reason returned by
%% for instance {@link file/2} or {@link proto_defs/2}.
-spec format_error(Err) -> iolist() when
Err :: reason() | {error, reason()} | {error, reason(), [warning()]}.
format_error({error, Reason, _Warns}) -> fmt_err(Reason);
format_error({error, Reason}) -> fmt_err(Reason);
format_error(Reason) -> fmt_err(Reason).
%% Note: do NOT include trailing newline (\n or ~n)
fmt_err({option_error, {not_supported, maps_omitted_nif}}) ->
?f("Options maps, maps_unset_optional=omitted and nif is not supported");
fmt_err({parse_error, FileName, {Line, Module, ErrInfo}}) ->
?f("~s:~w: ~s", [FileName, Line, Module:format_error(ErrInfo)]);
fmt_err({scan_error, FileName, {Line, Module, ErrInfo}}) ->
?f("~s:~w: ~s", [FileName, Line, Module:format_error(ErrInfo)]);
fmt_err({import_not_found, Import, Tried}) ->
PrettyTried = [begin
PrettyReason = file:format_error(Reason),
?f("~n ~ts (~s (~p))", [File,PrettyReason,Reason])
end
|| {File,Reason} <- Tried],
TriedTxt = if Tried == [] -> "";
true -> ", tried:"
end,
?f("Could not find import file ~p~s~s", [Import, TriedTxt, PrettyTried]);
fmt_err({fetcher_issue, File, Reason}) ->
?f("Failed to import file ~p using fetcher, ~p", [File, Reason]);
fmt_err({read_failed, File, Reason}) ->
?f("failed to read ~p: ~s (~p)", [File, file:format_error(Reason), Reason]);
fmt_err({post_process, Reasons}) ->
gpb_parse:format_post_process_error({error, Reasons});
fmt_err({write_failed, File, Reason}) ->
?f("failed to write ~s: ~s (~p)", [File, file:format_error(Reason),Reason]);
fmt_err({invalid_options,any_translate,nif}) ->
"Option error: Not supported: both any_translate and nif";
fmt_err({invalid_options, epb_functions, maps}) ->
"Option error: Not supported: both epb_compatibility (or epb_functions) "
"and maps";
fmt_err({epb_compatibility_impossible, {with_msg_named, msg}}) ->
"Not possible to generate epb compatible functions when a message "
"is named 'msg' because of collision with the standard gpb functions "
"'encode_msg' and 'decode_msg'";
fmt_err({rename_defs, Reason}) ->
gpb_names:format_error(Reason);
fmt_err(X) ->
?f("Unexpected error ~p", [X]).
%% @doc Produce a plain-text error message from a reason returned by
%% for instance {@link file/2} or {@link proto_defs/2}.
%% @end
%% Note: do NOT include trailing newline (\n or ~n)
-spec format_warning(warning()) -> iolist().
format_warning(cyclic_message_dependencies) ->
?f("Warning: omitting type specs due to cyclic message references.");
format_warning({ignored_field_opt_packed_for_unpackable_type,
MsgName, FName, Type, _Opts}) ->
?f("Warning: ignoring option packed for non-packable field ~s.~s "
"of type ~p", [MsgName, FName, Type]);
format_warning(X) ->
case io_lib:deep_char_list(X) of
true -> X;
false -> ?f("Warning: Unknown warning: ~p", [X])
end.
%% @doc Command line interface for the compiler.
%% With no proto file to compile, print a help message and exit.
-spec c() -> no_return().
c() ->
io:format("No proto files specified.~n"),
show_help(),
halt(0).
%% @doc This function is intended as a command line interface for the compiler.
%% Call it from the command line as follows:
%% ```
%% erl <erlargs> [gpb-opts] -s gpb_compile c File.proto ...
%% erl <erlargs> -s gpb_compile c File.proto ... -extra [gpb-opts]
%% '''
%% The `<erlargs>' can be `-noshell -noinput +B -boot start_clean -pa SomeDir'
%%
%% The options below are supported. Dashes and underscores inside option names
%% are equivalent, ie `-o-erl' and `-o_erl' are the same option.
%% <dl>
%% <dt>`-IDir' `-I Dir'</dt>
%% <dd>Specify include directory.
%% Option may be specified more than once to specify
%% several include directories.</dd>
%% <dt>`-o Dir'</dt>
%% <dd>Specify output directory for where to generate
%% the <i>ProtoFile</i>.erl and <i>ProtoFile</i>.hrl</dd>
%% <dt>`-o-erl Dir' | `-o-hrl Dir' | `-o-nif-cc Dir'</dt>
%% <dd>Specify output directory for where to generate
%% the <i>ProtoFile</i>.erl and <i>ProtoFile</i>.hrl respectively,
%% and for the NIF C++ file, if the `-nif' option is specified.
%% The `-o-erl Dir' option overrides any `-o Dir' option, and
%% similarly for the other file-type specific output options.</dd>
%% <dt>`-v optionally | always | never'</dt>
%% <dd>Specify how the generated encoder should
%% verify the message to be encoded.</dd>
%% <dt>`-nif'</dt>
%% <dd>Generate nifs for linking with the protobuf C(++) library.</dd>
%% <dt>`-load_nif FunctionDefinition'</dt>
%% <dd>Specify `FunctionDefinition' as the text that defines the
%% function `load_nif/0'. This is called as the `on_load'
%% hook for loading the NIF. See also the doc for the `load_nif'
%% option in the {@link file/2} function.</dd>
%% <dt>`-c true | false | auto | integer() | float()'</dt>
%% <dd>Specify how or when the generated decoder should
%% copy fields of type `bytes'. See the `copy_bytes' option
%% for the function {@link file/2} for more info.</dd>
%% <dt>`-strbin'</dt>
%% <dd>Specify that decoded strings should be returned as binaries,
%% instead of as strings (lists).</dd>
%% <dt>`-pldefs'</dt>
%% <dd>Specify that introspection functions shall return proplists
%% instead of `#field{}' records, to make the generated code
%% completely free of even compile-time dependencies to gpb.</dd>
%% <dt>`-pkgs'</dt>
%% <dd>Prepend the name of a package to every message it contains.
%% If no package is defined, nothing will be prepended.
%% Default is to not prepend package names for backwards
%% compatibility, but it is needed for some proto files.</dd>
%% <dt>`-any_translate MsFs'</dt>
%% <dd>Call functions in `MsFs' to pack, unpack, merge and verify
%% `google.protobuf.Any' messages. The `MsFs' is a string on the
%% following format: `e=Mod:Fn,d=Mod:Fn[,m=Mod:Fn][,V=Mod:Fn]'.
%% The specified modules and functinos are called and used as follows:
%% <dl>
%% <dt>e=Mod:Fn</dt>
%% <dd>Call `Mod:Fn(Term)' to pack the `Term' to
%% a `google.protobuf.Any' message.</dd>
%% <dt>d=Mod:Fn</dt>
%% <dd>Call `Mod:Fn(Any)' to unpack the `Any' to
%% unpack a `google.protobuf.Any' message to a term.</dd>
%% <dt>m=Mod:Fn</dt>
%% <dd>Call `Mod:Fn(Term1, Term2) -> Term3' to merge two
%% unpacked terms to a resulting Term3.</dd>
%% <dt>V=Mod:Fn</dt>
%% <dd>Call `Mod:Fn(Term) -> _' to verify an unpacked `Term'.
%% If `Term' is valid, the function is expected to just return
%% any value, which is ignored and discarded.
%% If `Term' is invalid, the function is exptected to not
%% return anything, but instead either crash, call
%% `erlang:error/1', or `throw/1' or `exit/1'. with the
%% reason for error.
%% If you want to use a verifier, this is the new preferred
%% approach.</dd>
%% <dt>v=Mod:Fn</dt>
%% <dd>Call `Mod:Fn(Term, ErrorF) -> _' to verify an unpacked `Term'.
%% This exists for backwards compatibility, and its use
%% is deprecated.</dd>.
%% </dl>
%% </dd>
%% <dt>`-msgprefix Prefix'</dt>
%% <dd>Prefix each message with `Prefix'. This can be useful to
%% when including different sub-projects that have colliding
%% message names.</dd>
%% <dt>`-modprefix Prefix'</dt>
%% <dd>Prefix each module with `Prefix'. Normally the module name of
%% the generated code is based on the name of the `.proto' file.
%% This option prepends a prefix to the module name, which can be
%% useful when including different sub-projects that have
%% colliding proto file names.</dd>
%% <dt>`-msgsuffix Suffix'</dt>
%% <dd>Suffix each message name with `Suffix'.</dd>
%% <dt>`-modsuffix Suffix'</dt>
%% <dd>Suffix each module name with `Suffix'.</dd>
%% <dt>`-modname Name'</dt>
%% <dd>Specify the name of the generated module.</dd>
%% <dt>`-msgtolower'</dt>
%% <dd>ToLower each message. Any prefixes/suffixes are added
%% after case modification.</dd>
%% <dt>`-il'</dt>
%% <dd>Generate code that include gpb.hrl using `-include_lib'
%% instead of `-include', which is the default.</dd>
%% <dt>`-type'<br/>`-no_type'</dt>
%% <dd>Enables or disables `::Type()' annotations in the generated code.
%% Default is to enable if there are no cyclic dependencies.</dd>
%% <dt>`-descr'</dt>
%% <dd>Generate self-description information.</dd>
%% <dt>`-maps'</dt>
%% <dd>This option expands to the following options:
%% <ul>
%% <li>`-msgs-as-maps'</li>
%% <li>`-mapfields-as-maps'</li>
%% <li>`-defs-as-maps'</li>
%% </ul>
%% See the `maps' option for the function {@link file/2}
%% for more info.</dd>
%% <dt>`-maps_unset_optional omitted | present_undefined'</dt>
%% <dd>Specifies the internal format for optional fields that are unset.</dd>
%% <dt>`-msgs-as-maps'</dt>
%% <dd>Specifies that messages should be maps. No `.hrl' file will
%% be generated.
%% Without this option, messages will be records.</dd>
%% <dt>`-mapfields-as-maps'</dt>
%% <dd>Specifies that fields of type `map<_,_>' should be maps.
%% Otherwise, they will be 2-tuples.</dd>
%% <dt>`-defs-as-maps'</dt>
%% <dd>Specifies that proto defintions from the generated code
%% are to be returned as maps. Otherwise, they will be lists
%% of tuples and records (or proplists if the `-pldefs' option
%% is specified)</dd>
%% <dt>`-erlc_compile_options Options'</dt>
%% <dd>Specifies compilation options, in a comma separated string, to pass
%% along to the `-compile(...)' directive on the generated code.</dd>
%% <dt>`-epb'</dt>
%% <dd>Enable compatibility with the Erlang Protobuffs library:
%% <ul>
%% <li>Implies the `-epb-functions' option</li>
%% <li>Implies the `-defaults-for-omitted-optionals' option</li>
%% <li>Implies the `-modsuffix _pb' option</li>
%% <li>Implies the `-msgtolower' option</li>
%% </ul></dd>
%% <dt>`-epb-functions'</dt>
%% <dd>For compatibility with the Erlang Protobuffs library, generate also
%% the following functions: `encode/1', `decode/2', `encode_MsgName/1'
%% and `decode_MsgName/1'</dd>
%% <dt>`-defaults-for-omitted-optionals'</dt>
%% <dd>For optional fields not present on decoding, set the field to
%% its default value, if any, instead of to `undefined'.</dd>
%% <dt>`-type-defaults-for-omitted-optionals'</dt>
%% <dd>For optional fields not present on decoding, set the field to
%% its type-default, instead of to `undefined'.</dd>
%% <dt>`-for-version N'</dt>
%% <dd>Generate code for Erlang/OTP version N instead of current.</dd>
%% <dt>`-Werror', `-W1', `-W0', `-W', `-Wall'</dt>
%% <dd>`-Werror' means treat warnings as errors<br></br>
%% `-W1' enables warnings, `-W0' disables warnings.<br></br>
%% `-W' and `-Wall' are the same as `-W1'</dd>
%% <dt>`--help' or `-h'</dt>
%% <dd>Show help.</dd>
%% <dt>`--version' or `-V'</dt>
%% <dd>Show the version number of gpb.</dd>
%% </dl>
%% If several files are specified, each is compiled individually, no
%% checking is done for instance for multiply defined messages or
%% fields across files, such as the `protoc' does.
-spec c([string() | atom()]) -> no_return().
c([F | _]=Files) when is_atom(F); is_list(F) -> %% invoked with -s or -run
erlang:system_flag(backtrace_depth, 32),
FileNames = [if is_atom(File) -> atom_to_list(File);
is_list(File) -> File
end
|| File <- Files],
InitArgs = init_args_to_argv(init:get_arguments()),
PlainArgs = init:get_plain_arguments(),
Argv = InitArgs ++ PlainArgs ++ FileNames,
case parse_opts_and_args(Argv) of
{ok, {Opts, Args}} ->
c(Opts, Args);
{error, Reason} ->
io:format("Error: ~s.~n", [Reason]),
show_args(),
halt(1)
end.
init_args_to_argv(InitArgs) ->
lists:append([["-"++atom_to_list(OptName) | OptArgs]
|| {OptName, OptArgs} <- InitArgs,
is_gpb_opt(OptName)]).
%% Opts are expected to be on same format as accepted by file/2.
%% passed by parse_opts_and_args/2.
-spec c(opts(), [ProtoFileName::string()]) -> no_return().
c(Opts, Args) ->
case determine_cmdline_op(Opts, Args) of
error ->
show_help(),
halt(1);
show_help ->
show_help(),
halt(0);
show_version ->
show_version(),
halt(0);
compile ->
Opts2 = Opts ++ [report_warnings, report_errors],
Results = [file(FileName, Opts2) || FileName <- Args],
case lists:usort(Results) of
[ok] -> halt(0);
_Errs -> halt(1)
end
end.
-spec parse_opts_and_args([string()]) -> {ok, {opts(), Args::[string()]}} |
{error, Reason::string()}.
parse_opts_and_args(Argv) ->
do_parse_argv(Argv, [], []).
do_parse_argv(["-"++OptName=Opt | Rest], Opts, Files) ->
case find_opt_spec(OptName) of
{ok, OptSpec} ->
case parse_opt(OptName, OptSpec, Rest) of
{ok, {ParsedOpt, Rest2}} ->
do_parse_argv(Rest2, [ParsedOpt | Opts], Files);
{error, Reason} ->
{error, Reason}
end;
error ->
{error, "Unknown option " ++ Opt}
end;
do_parse_argv([File | Rest], Opts, Files) ->
do_parse_argv(Rest, Opts, [File | Files]);
do_parse_argv([], Opts, Files) ->
{ok, {lists:reverse(Opts), lists:reverse(Files)}}.
is_gpb_opt(InitArgOptAtom) ->
find_opt_spec(atom_to_list(InitArgOptAtom)) /= error.
find_opt_spec(OptName) ->
case [OptSpec || OptSpec <- opt_specs(), opt_matches(OptName, OptSpec)] of
[] ->
error;
[OptSpec] ->
{ok, OptSpec}
end.
opt_matches(Opt, {OptName, 'string_maybe_appended()', _OptTag, _Descr}) ->
lists:prefix(norm_uscore_dash(OptName), norm_uscore_dash(Opt));
opt_matches(Opt, {OptName, _Type, _OptTag, _Descr}) ->
norm_uscore_dash(Opt) == norm_uscore_dash(OptName).
norm_uscore_dash("_"++Tl) -> "-" ++ norm_uscore_dash(Tl);
norm_uscore_dash([C | Tl]) -> [C | norm_uscore_dash(Tl)];
norm_uscore_dash("") -> "".
parse_opt(Opt, {OptName, 'string_maybe_appended()', OptTag, _Descr}, Rest) ->
case {Opt, Rest} of
{OptName, [H | Rest2]} ->
{ok, {{OptTag, H}, Rest2}};
{OptName, []} ->
{error, "Missing argument for option -" ++ OptName};
_ ->
true = lists:prefix(OptName, Opt),
OptArg = gpb_lib:string_slice(Opt, length(OptName)),
{ok, {{OptTag, OptArg}, Rest}}
end;
parse_opt(_, {_OptName, undefined, OptTag, _Descr}, Rest) ->
{ok, {OptTag, Rest}};
parse_opt(_, {_OptName, 'string()', OptTag, _Descr}, [OptArg | Rest]) ->
{ok, {{OptTag, OptArg}, Rest}};
parse_opt(_, {OptName, 'integer()', OptTag, _Descr}, [OptArg | Rest]) ->
try list_to_integer(OptArg) of
N -> {ok, {{OptTag, N}, Rest}}
catch error:badarg ->
{error, ?ff("Invalid version number (integer) for ~s: ~p",
[OptName, OptArg])}
end;
parse_opt(_, {_OptName, F, OptTag, _Descr}, Rest) when is_function(F) ->
F(OptTag, Rest);
parse_opt(_, {OptName, Alternatives, OptTag, _Descr}, [OptArg | Rest]) ->
case parse_opt_alts(tuple_to_list(Alternatives), OptArg, OptTag) of
{ok, Opt} -> {ok, {Opt, Rest}};
error -> {error, "Invalid argument for -" ++ OptName}
end;
parse_opt(OptName, _OptSpec, []) ->
{error, "Missing argument for option -" ++ OptName}.
parse_opt_alts(['number()' | Rest], OptArg, OptTag) ->
case string_to_number(OptArg) of
{ok, Value} -> {ok, {OptTag, Value}};
error -> parse_opt_alts(Rest, OptArg, OptTag)
end;
parse_opt_alts([Value | Rest], OptArg, OptTag) ->
case atom_to_list(Value) of
OptArg -> {ok, {OptTag, Value}};
_ -> parse_opt_alts(Rest, OptArg, OptTag)
end;
parse_opt_alts([], _OptArg, _OptTag) ->
error.
opt_specs() ->
[
{"I", 'string_maybe_appended()', i, "\n"
" Specify include directory.\n"
" Option may be specified more than once to specify\n"
" several include directories.\n"},
{"o", 'string()', o, "Dir\n"
" Specify output directory for where to generate\n"
" the <ProtoFile>.erl and <ProtoFile>.hrl\n"},
{"o-erl", 'string()', o_erl, "Dir\n"
" Specify output directory for where to generate\n"
" the <ProtoFile>.erl.\n"
" The -o-erl Dir option overrides any -o Dir option, and\n"
" similarly for the other file-type specific output options.\n"},
{"o-hrl", 'string()', o_hrl, "Dir\n"
" Specify output directory for where to generate\n"
" the <ProtoFile>.hrl\n"},
{"o-nif-cc", 'string()', o_nif_cc, "Dir\n"
" Specify output directory for where to generate\n"
" the NIF C++ file, if the -nif option is specified\n"},
{"nif", undefined, nif, "\n"
" Generate nifs for linking with the protobuf C(++) library.\n"},
{"load_nif", 'string()', load_nif, "FunctionDefinition\n"
" Specify FunctionDefinition as the text that defines the\n"
" function load_nif/0. This is called as the -on_load.\n"
" hook for loading the NIF.\n"},
{"v", {optionally, always, never}, verify, " optionally | always | never\n"
" Specify how the generated encoder should\n"
" verify the message to be encoded.\n"},
{"c", {true, false, auto, 'number()'}, copy_bytes,
" true | false | auto | number()\n"
" Specify how or when the generated decoder should\n"
" copy fields of type bytes.\n"},
{"strbin", undefined, strings_as_binaries, "\n"
" Specify that decoded strings should be returned as binaries,\n"
" instead of as strings (lists).\n"},
{"pldefs", undefined, defs_as_proplists, "\n"
" Specify that introspection functions shall return proplists\n"
" instead of #field{} records, to make the generated code\n"
" completely free of even compile-time dependencies to gpb.\n"},
{"pkgs", undefined, use_packages, "\n"
" Prepend the name of a package to every message it contains.\n"
" If no package is defined, nothing will be prepended.\n"
" Default is to not prepend package names for backwards\n"
" compatibility, but it is needed for some proto files.\n"},
{"any_translate", fun opt_any_translate/2, any_translate,
" e=Mod:Fn,d=Mod:Fn[,m=Mod:Fn][,v=Mod:Fn]\n"
" For a google.protobuf.Any message, call Mod:Fn to:\n"
" - encode (calls Mod:Fn(Term) -> AnyMessage to pack)\n"
" - decode (calls Mod:Fn(AnyMessage) -> Term to unpack)\n"
" - merge (calls Mod:Fn(Term,Term2) -> Term3 to merge unpacked)\n"
" - verify (calls Mod:Fn(Term) -> _ to verify unpacked)\n"},
{"msgprefix", 'string()', msg_name_prefix, "Prefix\n"
" Prefix each message with Prefix.\n"},
{"modprefix", 'string()', module_name_prefix, "Prefix\n"
" Prefix the module name with Prefix.\n"},
{"msgsuffix", 'string()', msg_name_suffix, "Suffix\n"
" Suffix each message with Suffix.\n"},
{"msgtolower", undefined, msg_name_to_lower, "ToLower\n"
" ToLower each message. Any prefixes/suffixes are added\n"
" after case modification.\n"},
{"modsuffix", 'string()', module_name_suffix, "Suffix\n"
" Suffix the module name with Suffix.\n"},
{"modname", 'string()', module_name, "Name\n"
" Specify the name of the generated module.\n"},
{"il", undefined, include_as_lib, "\n"
" Generate code that includes gpb.hrl using -include_lib\n"
" instead of -include, which is the default.\n"},
{"type", undefined, type_specs, "\n"
" Enables `::Type()' annotations in the generated code.\n"},
{"no_type", fun opt_no_type_specs/2, type_specs, "\n"
" Disbles `::Type()' annotations in the generated code.\n"},
{"descr", undefined, descriptor, "\n"
" Generate self-description information.\n"},
{"maps", undefined, maps, "\n"
" This will expand to the following options:\n"
" -msgs-as-maps\n"
" -msgfields-as-maps\n"
" -defs-as-maps\n"},
{"maps_unset_optional", {omitted, present_undefined}, maps_unset_optional,
"omitted | present_undefined\n"
" Specifies the internal format for optional fields\n"
" that are unset.\n"},
{"msgs-as-maps", undefined, msgs_as_maps, "\n"
" Specifies that messages should be maps.\n"
" Otherwise, they will be records.\n"},
{"mapfields-as-maps", undefined, mapfields_as_maps, "\n"
" Specifies that fields of type map<_,_> should be maps.\n"
" Otherwise, they will be 2-tuples.\n"},
{"defs-as-maps", undefined, defs_as_maps, "\n"
" Specifies that proto defintions from the generated code\n"
" are to be returned as maps. Otherwise, they will be lists\n"
" of tuples and records (or proplists if the -pldefs option\n"
" is specified)\n"},
{"erlc_compile_options", 'string()', erlc_compile_options, "String\n"
" Specifies compilation options, in a comma separated string, to\n"
" pass along to the -compile() directive on the generated code.\n"},
{"epb", undefined, epb_compatibility, "\n"
" Enable compatibility with the Erlang Protobuffs library:\n"
" * Implies the -epb-functions option\n"
" * Implies the -modsuffix _pb option\n"
" * Implies the -msgtolower option\n"},
{"epb-functions", undefined, epb_functions, "\n"
" Generate some functions for API compatibility with the\n"
" Erlang protobuffs library:\n"
" * encode/1 and encode_MsgName/1\n"
" * decode/2 and decode_MsgName/1\n"},
{"defaults-for-omitted-optionals", undefined,
defaults_for_omitted_optionals, "\n"
" For optional fields not present on decoding, set the field\n"
" to its default value, if any, instead of to undefined.\n"},
{"type-defaults-for-omitted-optionals", undefined,
type_defaults_for_omitted_optionals, "\n"
" For optional fields not present on decoding, set the field\n"
" to its type-default, instead of to undefined.\n"},
{"for-version", 'integer()', target_erlang_version, "N\n"
" Generate code for Erlang/OTP version N instead of current.\n"},
{"Werror",undefined, warnings_as_errors, "\n"
" Treat warnings as errors\n"},
{"W1", undefined, report_warnings, "\n"
" Report warnings\n"},
{"W0", undefined, {report_warnings,false}, "\n"
" Do not report warnings\n"},
{"Wall", undefined, report_warnings, "\n"
" Same as -W1\n"},
{"W", undefined, report_warnings, "\n"
" Same as -W1\n"},
{"h", undefined, help, "\n"
" Show help\n"},
{"-help", undefined, help, "\n"
" Show help\n"},
{"V", undefined, version, "\n"
" Show version\n"},
{"-version", undefined, version, "\n"
" Show version\n"}
] ++
case os:getenv("GPB_DEV_OPTS") of
"true" ->
[{"fp", {pass_as_params,pass_as_record}, field_pass_method,
"pass_as_params | pass_as_record\n"
" Override whether message fields are to be passed\n"
" as parameters or as a record (or map, depending\n"
" on the -maps option). This is purely internal,\n"
" and has no impact neither on input nor output,\n"
" but there may be a performance difference.\n"
" Normally, it is calculated automatically for each\n"
" message, but during development it may be useful\n"
" to be able to force it.\n"}];
_ ->
[]
end.
opt_no_type_specs(OptTag, Rest) ->
Opt = {OptTag, false},
{ok, {Opt, Rest}}.
opt_any_translate(OptTag, [S | Rest]) ->
try
Ts = gpb_lib:string_lexemes(S, ","),
Opt = {OptTag, [opt_any_translate_mfa(T) || T <- Ts]},
{ok, {Opt, Rest}}
catch throw:{badopt,ErrText} ->
{error, ErrText}
end.
opt_any_translate_mfa("e="++MF) -> {encode,opt_mf_str(MF, 1)};
opt_any_translate_mfa("d="++MF) -> {decode,opt_mf_str(MF, 1)};
opt_any_translate_mfa("m="++MF) -> {merge, opt_mf_str(MF, 2)};
opt_any_translate_mfa("V="++MF) -> {verify,opt_mf_str(MF, 1)};
opt_any_translate_mfa("v="++MF) -> {verify,opt_mf_str_verify(MF)};
opt_any_translate_mfa(X) -> throw({badopt,"Invalid translation spec: "++X}).
opt_mf_str(S, Arity) ->
case gpb_lib:string_lexemes(S, ":") of
[M,F] -> {list_to_atom(M),list_to_atom(F),opt_arg_template(Arity)};
_ -> throw({badopt,"Invalid Mod:Fn spec: "++S})
end.
opt_mf_str_verify(S) ->
{M,F,[A]} = opt_mf_str(S, 1),
{M,F,[A,'$errorf']}.
opt_arg_template(Arity) ->
[list_to_atom(?ff("$~w", [I])) || I <- lists:seq(1,Arity)].
determine_cmdline_op(Opts, FileNames) ->
case {lists:member(help, Opts), lists:member(version, Opts)} of
{true, _} -> show_help;
{_, true} -> show_version;
_ -> if FileNames == [] -> error;
FileNames /= [] -> compile
end
end.
show_help() ->
io:format(
"gpb version ~s~n"
"Usage: erl <erlargs> [gpb-opts] -s ~p c <ProtoFile>.proto~n"
" or: erl <erlargs> -s ~p c <ProtoFile>.proto -extra [gpb-opts]~n"
"Typical erlargs = -noshell -noinput +B -boot start_clean -pa SomeDir~n"
"~n",
[gpb:version_as_string(), ?MODULE, ?MODULE]),
show_args().
show_arg({OptDef, 'string_maybe_appended()', _, OptDoc}) ->
io:format(" -~s -~sOption ~s", [OptDef, OptDef, OptDoc]);
show_arg({OptDef, _, _, OptDoc}) ->
io:format(" -~s ~s", [OptDef, OptDoc]).
-spec show_args() -> _. % side effect is to print valid opts/args
show_args() ->
io:format(
"Recognized gpb-opts: (see the edoc for ~p for further details)~n",
[?MODULE]),
lists:foreach(fun show_arg/1, opt_specs()).
-spec show_version() -> _. % side effect is to print version
show_version() ->
io:format("gpb version ~s~n", [gpb:version_as_string()]).
string_to_number(S) ->
try {ok, list_to_integer(S)}
catch error:badarg ->
try {ok, list_to_float(S)}
catch error:badarg -> error
end
end.
parse_file_or_string(In, Opts) ->
Opts1 = add_curr_dir_as_include_if_needed(Opts),
case parse_file_and_imports(In, Opts1) of
{ok, {Defs1, _AllImported}} ->
case gpb_parse:post_process_all_files(Defs1, Opts1) of
{ok, Defs2} ->
{ok, Defs2};
{error, Reasons} ->
{error, {post_process, Reasons}}
end;
{error, Reason} ->
{error, Reason}
end.
add_curr_dir_as_include_if_needed(Opts) ->
ImportDirs = [Dir || {i,Dir} <- Opts],
case lists:member(".", ImportDirs) of
true -> Opts;
false -> Opts ++ [{i,"."}]
end.
parse_file_and_imports(In, Opts) ->
FName = file_name_from_input(In),
parse_file_and_imports(In, [FName], Opts).
file_name_from_input({Mod,_S}) -> lists:concat([Mod, ".proto"]);
file_name_from_input(FName) -> FName.
parse_file_and_imports(In, AlreadyImported, Opts) ->
case locate_read_import_int(In, Opts) of
{ok, Contents} ->
%% Add to AlreadyImported to prevent trying to import it again: in
%% case we get an error we don't want to try to reprocess it later
%% (in case it is multiply imported) and get the error again.
FName = file_name_from_input(In),
AlreadyImported2 = [FName | AlreadyImported],
case scan_and_parse_string(Contents, FName, Opts) of
{ok, Defs} ->
Imports = gpb_parse:fetch_imports(Defs),
Opts2 = ensure_include_path_to_wellknown_types_if_proto3(
Defs, Imports, Opts),
read_and_parse_imports(Imports, AlreadyImported2,
Defs, Opts2);
{error, Reason} ->
{error, Reason}
end;
{error, Reason} ->
{error, Reason}
end.
scan_and_parse_string(S, FName, Opts) ->
case gpb_scan:string(S) of
{ok, Tokens, _} ->
case gpb_parse:parse(Tokens++[{'$end', 999}]) of
{ok, PTree} ->
case gpb_parse:post_process_one_file(FName, PTree, Opts) of
{ok, Result} ->
{ok, Result};
{error, Reason} ->
{error, {parse_error, FName, Reason}}
end;
{error, {_Line, _Module, _ErrInfo}=Reason} ->
{error, {parse_error, FName, Reason}}
end;
{error, {_Line0, _Module, _ErrInfo}=Reason, _Line1} ->
{error, {scan_error, FName, Reason}}
end.
read_and_parse_imports([Import | Rest], AlreadyImported, Defs, Opts) ->
case lists:member(Import, AlreadyImported) of
true ->
read_and_parse_imports(Rest, AlreadyImported, Defs, Opts);
false ->
case import_it(Import, AlreadyImported, Defs, Opts) of
{ok, {Defs2, Imported2}} ->
read_and_parse_imports(Rest, Imported2, Defs2, Opts);
{error, Reason} ->
{error, Reason}
end
end;
read_and_parse_imports([], Imported, Defs, _Opts) ->
{ok, {Defs, Imported}}.
import_it(Import, AlreadyImported, Defs, Opts) ->
%% FIXME: how do we handle scope of declarations,
%% e.g. options/package for imported files?
case parse_file_and_imports(Import, AlreadyImported, Opts) of
{ok, {MoreDefs, MoreImported}} ->
Defs2 = Defs++MoreDefs,
Imported2 = lists:usort(AlreadyImported++MoreImported),
{ok, {Defs2, Imported2}};
{error, Reason} ->
{error, Reason}
end.
locate_read_import_int({_Mod, Str}, _Opts) ->
{ok, Str};
locate_read_import_int(Import, Opts) ->
case proplists:get_value(import_fetcher, Opts) of
undefined ->
locate_read_import_aux(Import, Opts);
Importer when is_function(Importer, 1) ->
case Importer(Import) of
from_file ->
locate_read_import_aux(Import, Opts);
{ok, Contents} when is_list(Contents) ->
case lists:all(fun is_integer/1, Contents) of
true ->
{ok, Contents};
false ->
error({bad_fetcher_return,
{not_a_string, Contents},
Import})
end;
{error, Reason} ->
{error, {fetcher_issue, Import, Reason}};
X ->
error({bad_fetcher_return, Import, X})
end
end.
locate_read_import_aux(Import, Opts) ->
ImportPaths = [Path || {i, Path} <- Opts],
case locate_import_aux(ImportPaths, Import, Opts, []) of
{ok, File} ->
read_import(File, Opts);
{error, _} = Error ->
Error
end.
%% @doc Locate an import target. This function might be potentially
%% useful for instance in an intercepting `import_fetcher' fun that
%% just wants to record the accessed imports.
-spec locate_import(string(), opts()) -> {ok, File::string()} |
{error, reason()}.
locate_import(ProtoFileName, Opts) ->
Opts1 = ensure_include_path_to_wellknown_types(Opts),
ImportPaths = [Path || {i, Path} <- Opts1],
locate_import_aux(ImportPaths, ProtoFileName, Opts1, []).
locate_import_aux([Path | Rest], Import, Opts, Tried) ->
File = filename:join(Path, Import),
case file_read_file_info(File, Opts) of
{ok, #file_info{access = A}} when A == read; A == read_write ->
{ok, File};
{ok, #file_info{}} ->
locate_import_aux(Rest, Import, Opts, Tried);
{error, Reason} ->
locate_import_aux(Rest, Import, Opts, [{File,Reason} | Tried])
end;
locate_import_aux([], Import, _Opts, Tried) ->
{error, {import_not_found, Import, Tried}}.
%% @doc Read an import file. This function might be potentially
%% useful for instance in an intercepting `import_fetcher' fun that
%% just wants to record the accessed imports.
-spec read_import(string(), opts()) -> {ok, string()} | {error, reason()}.
read_import(File, Opts) ->
case file_read_file(File, Opts) of
{ok,B} ->
case utf8_decode(B) of
{ok, {utf8, S}} ->
{ok, S};
{ok, {latin1, S}} ->
{ok, S};
{error, Reason} ->
{error, {utf8_decode_failed, Reason, File}}
end;
{error, Reason} ->
{error, {read_failed, File, Reason}}
end.
ensure_include_path_to_wellknown_types_if_proto3(Defs, Imports, Opts) ->
case proplists:get_value(syntax, Defs) of
"proto3" ->
case lists:any(fun imports_wellknown/1, Imports) of
true ->
ensure_include_path_to_wellknown_types(Opts);
false ->
Opts
end;
_ ->
Opts
end.
ensure_include_path_to_wellknown_types(Opts) ->
PrivDir = get_priv_dir(),
Wellknown = filename:join(PrivDir, "proto3"),
sanity_check_installation_wellknown_proto3(Wellknown),
add_opt_unless_present({i,Wellknown}, Opts).
imports_wellknown("google/protobuf/"++_) -> true;
imports_wellknown(_) -> false.
add_opt_unless_present(Opt, [Opt | Rest]) ->
[Opt | Rest];
add_opt_unless_present(Opt, [H | Rest]) ->
[H | add_opt_unless_present(Opt, Rest)];
add_opt_unless_present(Opt, []) ->
[Opt].
get_priv_dir() ->
case application:get_application(?MODULE) of
{ok,CurrApp} ->
code:priv_dir(CurrApp);
undefined ->
%% Not loaded as an application, just executing code;
%% from an escript possibly? (or even from an ez archive?)
MDir = filename:dirname(code:which(?MODULE)),
case filename:basename(MDir) of
"ebin" ->
filename:join(filename:dirname(MDir), "priv");
_ ->
case code:priv_dir(gpb) of % hard-wired app name...
Dir when is_list(Dir) ->
Dir;
{error,Reason} ->
error({failed_to_locate_privdir,Reason})
end
end
end.
sanity_check_installation_wellknown_proto3(WellknownDir) ->
case filelib:is_dir(WellknownDir) of
true ->
ok;
false ->
error({well_known_proto3_missing,
"Your installation is missing the priv/proto3 "
"directory, which is expected to house the "
"'proto3 well known types' such as "
"google/protobuf/timestamp.proto and "
"google/protobuf/duration.proto. "
"They were expected (calculated) to be found in "
++ WellknownDir})
end.
try_topsort_defs(Defs) ->
G = digraph:new(),
%% Build a dependency graph {msg|group, Name} -> {msg|group,Name}
[digraph:add_vertex(G, _Key={Type,Name})
|| {Type,Name,_Fields} <- gpb_lib:msgs_or_groups(Defs)],
gpb_lib:fold_msg_or_group_fields(
fun(Type, From, #?gpb_field{type={msg,To}}, _) ->
digraph:add_edge(G, {Type,From}, {msg,To});
(Type, From, #?gpb_field{type={group,To}}, _) ->
digraph:add_edge(G, {Type,From}, {group,To});
(Type, From, #?gpb_field{type={map,_,{msg, To}}}, _) ->
digraph:add_edge(G, {Type,From}, {msg,To});
(_Type, _MsgName, _Feild, _Acc) ->
ok
end,
ok,
Defs),
case digraph_utils:topsort(G) of
false ->
digraph:delete(G),
{false, Defs};
Order ->
digraph:delete(G),
ROrder = lists:reverse(Order),
OrderedMsgOrGroupDefs =
[lists:keyfind(Key,1,Defs) || Key <- ROrder],
{true, OrderedMsgOrGroupDefs ++ (Defs -- OrderedMsgOrGroupDefs)}
end.
possibly_adjust_typespec_opt(IsAcyclic, Opts0) ->
CyclicDeps = not IsAcyclic,
TypeSpecs = gpb_lib:get_type_specs_by_opts(Opts0),
Opts1 = lists:keydelete(type_specs, 1, Opts0 -- [type_specs]),
if not CyclicDeps, TypeSpecs == preferably ->
{[], [{type_specs, true} | Opts1]};
CyclicDeps, TypeSpecs == preferably ->
Opts2 = [{type_specs, false} | Opts1],
{[], Opts2};
not CyclicDeps ->
{[], Opts0};
CyclicDeps, TypeSpecs ->
{[cyclic_message_dependencies], [{type_specs, false} | Opts1]};
CyclicDeps, not TypeSpecs ->
{[], Opts0}
end.
%% Input .proto file appears to be expected to be UTF-8 by Google's protobuf.
%% In 3.0.0, it accepts a byte order mark (BOM), but in 2.6.1 it does not.
%% It only accepts a BOM for for UTF-8. It does not accept UTF-16 nor UTF-32
%% input (tried both little and big endian for both, with proper BOMs).
utf8_decode(B) ->
{Enc, Len} = unicode:bom_to_encoding(B),
<<_Bom:Len/binary, B2/binary>> = B,
if Enc == latin1;
Enc == utf8 ->
%% Enc == latin1 means just that no Byte order mark was seen,
%% it might still be UTF-8 encoded, though, so try that first.
case unicode:characters_to_list(B2) of
S when is_list(S) ->
{ok, {utf8, S}};
{error, _, _} ->
{ok, {latin1, binary_to_list(B2)}}
end;
true ->
{error, {invalid_proto_byte_order_mark, Enc}}
end.
check_unpackables_marked_as_packed(Defs) ->
gpb_lib:fold_msg_or_group_fields(
fun(_, MsgName, #?gpb_field{name=FName, type=Type, opts=Opts}, Acc) ->
case {lists:member(packed, Opts), gpb:is_type_packable(Type)} of
{true, false} ->
Warn = {ignored_field_opt_packed_for_unpackable_type,
MsgName, FName, Type, Opts},
[Warn | Acc];
_ ->
Acc
end
end,
[],
Defs).
%% -- generating code ----------------------------------------------
format_erl(Mod, Defs, #anres{maps_as_msgs=MapsAsMsgs}=AnRes, Opts) ->
DoNif = proplists:get_bool(nif, Opts),
NoNif = not DoNif,
AsLib = proplists:get_bool(include_as_lib, Opts),
CompileOptsStr = get_erlc_compile_options_str(Opts),
gpb_lib:iolist_to_utf8_or_escaped_binary(
[?f("%% Automatically generated, do not edit~n"
"%% Generated by ~p version ~s~n",
[?MODULE, gpb:version_as_string()]),
?f("-module(~w).~n", [Mod]),
case CompileOptsStr of
"" -> "";
[_|_] -> ?f("-compile([~ts]).~n", [CompileOptsStr])
end,
"\n",
case gpb_lib:get_records_or_maps_by_opts(Opts) of
records -> ?f("-export([encode_msg/1, encode_msg/2]).~n");
maps -> ?f("-export([encode_msg/2, encode_msg/3]).~n")
end,
[[?f("-export([encode/1]). %% epb compatibility~n"),
[?f("-export([~p/1]).~n", [gpb_lib:mk_fn(encode_, MsgName)])
|| {{msg,MsgName}, _Fields} <- Defs],
"\n"]
|| gpb_lib:get_epb_functions_by_opts(Opts)],
?f("-export([decode_msg/2"),[", decode_msg/3" || NoNif], ?f("]).~n"),
case gpb_lib:get_records_or_maps_by_opts(Opts) of
records -> ?f("-export([merge_msgs/2, merge_msgs/3]).~n");
maps -> ?f("-export([merge_msgs/3, merge_msgs/4]).~n")
end,
[[?f("-export([decode/2]). %% epb compatibility~n"),
[?f("-export([~p/1]).~n", [gpb_lib:mk_fn(decode_, MsgName)])
|| {{msg,MsgName}, _Fields} <- Defs],
"\n"]
|| gpb_lib:get_epb_functions_by_opts(Opts)],
case gpb_lib:get_records_or_maps_by_opts(Opts) of
records -> ?f("-export([verify_msg/1, verify_msg/2]).~n");
maps -> ?f("-export([verify_msg/2, verify_msg/3]).~n")
end,
?f("-export([get_msg_defs/0]).~n"),
?f("-export([get_msg_names/0]).~n"),
?f("-export([get_group_names/0]).~n"),
?f("-export([get_msg_or_group_names/0]).~n"),
?f("-export([get_enum_names/0]).~n"),
?f("-export([find_msg_def/1, fetch_msg_def/1]).~n"),
?f("-export([find_enum_def/1, fetch_enum_def/1]).~n"),
gpb_gen_introspect:format_enum_value_symbol_converter_exports(Defs),
?f("-export([get_service_names/0]).~n"),
?f("-export([get_service_def/1]).~n"),
?f("-export([get_rpc_names/1]).~n"),
?f("-export([find_rpc_def/2, fetch_rpc_def/2]).~n"),
?f("-export([get_package_name/0]).~n"),
[?f("-export([descriptor/0]).~n")
|| gpb_lib:get_gen_descriptor_by_opts(Opts)],
?f("-export([gpb_version_as_string/0, gpb_version_as_list/0]).~n"),
"\n",
[["-on_load(load_nif/0).\n",
"-export([load_nif/0]). %% for debugging of nif loading\n",
"\n"]
|| DoNif],
case gpb_lib:get_records_or_maps_by_opts(Opts) of
records -> ?f("-include(\"~s.hrl\").~n", [Mod]);
maps -> ""
end,
case gpb_lib:get_defs_as_maps_or_records(Opts) of
records ->
[case gpb_lib:get_field_format_by_opts(Opts) of
fields_as_records ->
if AsLib ->
?f("-include_lib(\"gpb/include/gpb.hrl\").~n");
not AsLib ->
?f("-include(\"gpb.hrl\").~n")
end;
fields_as_proplists ->
"";
fields_as_maps ->
""
end];
maps ->
""
end,
"\n",
gpb_gen_types:format_export_types(Defs, Opts),
"\n",
if not DoNif ->
case gpb_lib:get_2tuples_or_maps_for_maptype_fields_by_opts(Opts)
of
'2tuples' ->
gpb_gen_types:format_maps_as_msgs_record_defs(
MapsAsMsgs);
maps ->
""
end;
DoNif ->
""
end,
[[?f("~s~n", [gpb_gen_nif:format_load_nif(Mod, Opts)]),
"\n"]
|| DoNif],
%% Enabling inlining seems to cause performance to drop drastically
%% I've seen decoding performance go down from 76000 msgs/s
%% to about 10000 msgs/s for a set of mixed message samples.
%% f("-compile(inline).~n"),
%%
gpb_gen_encoders:format_encoders_top_function(Defs, Opts),
"\n",
if DoNif ->
?f("~s~n", [gpb_gen_nif:format_nif_encoder_error_wrappers(
Defs, AnRes, Opts)]);
not DoNif ->
[gpb_gen_encoders:format_msg_encoders(Defs, AnRes, Opts,
true),
gpb_gen_encoders:format_map_encoders(MapsAsMsgs, AnRes, Opts,
false),
gpb_gen_encoders:format_aux_encoders(Defs, AnRes, Opts)]
end,
"\n",
gpb_gen_decoders:format_decoders_top_function(Defs, Opts),
"\n\n",
if DoNif ->
[gpb_gen_nif:format_nif_decoder_error_wrappers(Defs,
AnRes, Opts)];
not DoNif ->
[gpb_gen_decoders:format_msg_decoders(Defs, AnRes, Opts),
gpb_gen_decoders:format_map_decoders(MapsAsMsgs, AnRes, Opts),
gpb_gen_decoders:format_aux_decoders(Defs, AnRes, Opts)]
end,
"\n",
gpb_gen_mergers:format_msg_merge_code(Defs, AnRes, Opts),
"\n",
gpb_gen_verifiers:format_verifiers_top_function(Defs, Opts),
"\n",
gpb_gen_verifiers:format_verifiers(Defs, AnRes, Opts),
"\n",
if not DoNif ->
[gpb_gen_translators:format_aux_transl_helpers(AnRes),
gpb_gen_translators:format_translators(Defs, AnRes, Opts)];
DoNif ->
[gpb_gen_translators:format_aux_transl_helpers(AnRes),
gpb_gen_translators:format_merge_translators(Defs, AnRes,
Opts)]
end,
"\n",
gpb_gen_introspect:format_introspection(Defs, Opts),
"\n",
possibly_format_descriptor(Defs, Opts),
"\n",
?f("gpb_version_as_string() ->~n"),
?f(" \"~s\".~n", [gpb:version_as_string()]),
"\n",
?f("gpb_version_as_list() ->~n"),
?f(" ~s.~n", [gpb_version_as_list_pretty()])],
Opts).
gpb_version_as_list_pretty() ->
%% The version "2.2-60-gb0decf3" is rendered with ~w
%% as: [2,2,0,0,60,[103,98,48,100,101,99,102,51]]
%% this function renders it as [2,2,0,0,60,"gb0decf3"]
%% which is exactly the same, but easier for humans to read.
{V, SubStrs} =
lists:mapfoldl(fun(N, Acc) when is_integer(N) -> {N, Acc};
(S, Acc) when is_list(S) -> {x, Acc++[S]}
end,
[],
gpb:version_as_list()),
S2 = remove_whitespaces(?ff("~p~n", [V])),
r_strs(S2, $x, SubStrs).
remove_whitespaces(S) -> [C || C <- S, not is_whitespace_char(C)].
is_whitespace_char($\s) -> true;
is_whitespace_char($\t) -> true;
is_whitespace_char($\n) -> true;
is_whitespace_char(_) -> false.
r_strs([M | Tl], M, [S|Rest]) -> ?ff("~p", [S]) ++ r_strs(Tl, M, Rest);
r_strs([C | Tl], M, SubStrs) -> [C | r_strs(Tl, M, SubStrs)];
r_strs("", _M, []) -> "".
get_erlc_compile_options_str(Opts) ->
proplists:get_value(erlc_compile_options, Opts, "").
%% -- descr -----------------------------------------------------
possibly_format_descriptor(Defs, Opts) ->
case gpb_lib:get_gen_descriptor_by_opts(Opts) of
true ->
try gpb_compile_descr:encode_defs_to_descriptor(Defs) of
Bin when is_binary(Bin) ->
gpb_codegen:format_fn(
descriptor, fun() -> 'bin' end,
[replace_term(bin, Bin)])
catch error:undef ->
ST = erlang:get_stacktrace(),
case {element(1,hd(ST)), element(2,hd(ST))} of
{gpb_compile_descr, encode_defs_to_descriptor} ->
gpb_codegen:format_fn(
descriptor,
fun() -> erlang:error(descr_not_avail) end);
_ ->
%% other error
erlang:raise(error, undef, ST)
end
end;
false ->
""
end.
%% -- hrl -----------------------------------------------------
possibly_format_hrl(Mod, Defs, Opts) ->
case gpb_lib:get_records_or_maps_by_opts(Opts) of
records -> format_hrl(Mod, Defs, Opts);
maps -> '$not_generated'
end.
format_hrl(Mod, Defs, Opts) ->
ModVsn = list_to_atom(atom_to_list(Mod) ++ "_gpb_version"),
gpb_lib:iolist_to_utf8_or_escaped_binary(
[?f("%% Automatically generated, do not edit~n"
"%% Generated by ~p version ~s~n",
[?MODULE, gpb:version_as_string()]),
"\n",
?f("-ifndef(~p).~n", [Mod]),
?f("-define(~p, true).~n", [Mod]),
"\n",
?f("-define(~p, \"~s\").~n", [ModVsn, gpb:version_as_string()]),
"\n",
gpb_lib:nl_join(
[gpb_gen_types:format_msg_record(Msg, Fields, Opts, Defs)
|| {_,Msg,Fields} <- gpb_lib:msgs_or_groups(Defs)]),
"\n",
?f("-endif.~n")],
Opts).
%% -- nif c++ code -----------------------------------------------------
possibly_format_nif_cc(Mod, Defs, AnRes, Opts) ->
case proplists:get_bool(nif, Opts) of
true -> gpb_gen_nif:format_nif_cc(Mod, Defs, AnRes, Opts);
false -> '$not_generated'
end.
%% -- compile to memory -----------------------------------------------------
compile_to_binary(Mod, HrlText, ErlCode, PossibleNifCode, Opts) ->
ModAsStr = flatten_iolist(?f("~p", [Mod])),
ErlCode2 = nano_epp(ErlCode, ModAsStr, HrlText),
{ok, Toks, _EndLine} = erl_scan:string(ErlCode2),
FormToks = split_toks_at_dot(Toks),
Forms = [case erl_parse:parse_form(Ts) of
{ok, Form} ->
Form;
{error, Reason} ->
io:format(user, "Ts=~p~n", [Ts]),
erlang:error(
{internal_error,?MODULE,Mod,Ts,Reason,
{more_info,[{full_erl,ErlCode2},{hrl,HrlText},
{nif,PossibleNifCode},{opts,Opts}]}})
end
|| Ts <- FormToks],
combine_erl_and_possible_nif(compile:noenv_forms(Forms, Opts),
PossibleNifCode).
nano_epp(Code, ModAsStr, HrlText) ->
%% nepp = nano-erlang-preprocessor. Couldn't find a way to run
%% the epp from a string, and don't want or need to use the file
%% system when everything is already in memory.
D0 = dict:new(),
{Txt, _EndLine, _Defs} = nepp1(Code, ModAsStr, HrlText, 1, D0, []),
Txt.
nepp1(<<"%% -*- coding:",_/binary>>=B, ModAsStr, HrlText, N, Ds, Acc) ->
%% First (non-coding) line must be a -file(...) directive,
%% or else unused record definitions in included files will
%% produce warnings: eg: {27,erl_lint,{unused_record,gpb_oneof}}.
{CodingLine,Rest} = read_until(B, "\n", ""),
Erl = (ModAsStr -- "''") ++ ".erl",
CodingAndFileDirective = CodingLine ++ "\n" ++ file_directive(Erl, 1),
Acc2 = lists:reverse(CodingAndFileDirective, Acc),
nepp2_nl(Rest, ModAsStr, HrlText, N, Ds, Acc2);
nepp1(Rest, ModAsStr, HrlText, N, Ds, Acc) ->
Erl = (ModAsStr -- "''") ++ ".erl",
FileDirective = file_directive(Erl, 1),
Acc2 = lists:reverse(FileDirective, Acc),
nepp2_nl(Rest, ModAsStr, HrlText, N, Ds, Acc2).
nepp2(<<"?MODULE", Rest/binary>>, ModAsStr, Hrl, N, Ds, Acc) ->
nepp2(Rest, ModAsStr, Hrl, N, Ds, lists:reverse(ModAsStr, Acc));
nepp2(<<$\n, Rest/binary>>, ModAsStr, Hrl, N, Ds, Acc) ->
nepp2_nl(Rest, ModAsStr, Hrl, N+1, Ds, [$\n | Acc]);
nepp2(<<C, Rest/binary>>, ModAsStr, Hrl, N, Ds, Acc) ->
nepp2(Rest, ModAsStr, Hrl, N, Ds, [C | Acc]);
nepp2(<<>>, _ModAsStr, _Hrl, N, Ds, Acc) ->
{lists:reverse(Acc), N, Ds}.
nepp2_nl(<<"-include", Rest/binary>>, ModAsStr, Hrl, N, Ds, Acc) ->
nepp2_inc(Rest, ModAsStr, Hrl, N, Ds, Acc);
nepp2_nl(<<"-include_lib", Rest/binary>>, ModAsStr, Hrl, N, Ds, Acc) ->
nepp2_inc(Rest, ModAsStr, Hrl, N, Ds, Acc);
nepp2_nl(<<"-define", Rest/binary>>, ModAsStr, Hrl, N, Ds, Acc) ->
nepp2_def(Rest, ModAsStr, Hrl, N, Ds, Acc);
nepp2_nl(<<"-ifdef", Rest/binary>>, ModAsStr, Hrl, N, Ds, Acc) ->
nepp2_ifdef(Rest, ifdef, ModAsStr, Hrl, N, Ds, Acc);
nepp2_nl(<<"-ifndef", Rest/binary>>, ModAsStr, Hrl, N, Ds, Acc) ->
nepp2_ifdef(Rest, ifndef, ModAsStr, Hrl, N, Ds, Acc);
nepp2_nl(<<"-endif.\n", Rest/binary>>, ModAsStr, Hrl, N, Ds, Acc) ->
nepp2_nl(Rest, ModAsStr, Hrl, N+1, Ds, Acc);
nepp2_nl(X, ModAsStr, Hrl, N, Ds, Acc) ->
nepp2(X, ModAsStr, Hrl, N, Ds, Acc).
nepp2_inc(Rest, ModAsStr, Hrl, N, Ds, Acc) ->
{_, Rest1} = read_until(Rest, "(", ""),
{Inc1, Rest2} = read_until(Rest1, ")", ""),
{_, Rest3} = read_until(Rest2, "\n", ""),
Inc = parse_term(Inc1),
Erl = (ModAsStr -- "''") ++ ".erl",
case classify_inc(Inc) of
gpb_hrl ->
FieldDef = field_record_to_text(),
OneofDef = oneof_record_to_text(),
RpcDef = rpc_record_to_text(),
Txt = lists:flatten([file_directive(Inc, 1),
FieldDef, OneofDef, RpcDef]),
Acc2 = lists:reverse(Txt ++ file_directive(Erl, N+1), Acc),
nepp2_nl(Rest3, ModAsStr, Hrl, N+1, Ds, Acc2);
mod_hrl when Hrl /= '$not_generated' ->
{Txt1, _End, Ds2} = nepp2_nl(Hrl, ModAsStr, Hrl, 1, Ds, []),
Txt2 = lists:flatten([file_directive(Inc, 1), Txt1]),
Acc2 = lists:reverse(Txt2 ++ file_directive(Erl, N+1), Acc),
nepp2_nl(Rest3, ModAsStr, Hrl, N+1, Ds2, Acc2)
end.
nepp2_def(Rest, ModAsStr, Hrl, N, Ds, Acc) ->
{_, Rest1} = read_until(Rest, "(", ""),
{Sym, Rest2} = read_until(Rest1, ",", ""),
{Val, Rest3} = read_until(Rest2, ")", ""),
{_, Rest4} = read_until(Rest3, "\n", ""),
Ds1 = dict:store(parse_term(Sym), parse_term(Val), Ds),
nepp2_nl(Rest4, ModAsStr, Hrl, N+1, Ds1, Acc).
nepp2_ifdef(Rest, SkipCond, ModAsStr, Hrl, N, Ds, Acc) ->
{_, Rest1} = read_until(Rest, "(", ""),
{Sym, Rest2} = read_until(Rest1, ")", ""),
{_, Rest3} = read_until(Rest2, "\n", ""),
case {dict:is_key(parse_term(Sym), Ds), SkipCond} of
{true, ifdef} -> nepp2_nl(Rest3, ModAsStr, Hrl, N+1, Ds, Acc);
{false, ifndef} -> nepp2_nl(Rest3, ModAsStr, Hrl, N+1, Ds, Acc);
_ -> nepp2_skip(Rest3, 1, ModAsStr, Hrl, N+1, Ds, Acc)
end.
nepp2_skip(<<"-endif.\n", Rest/binary>>, Depth, ModAsStr, Hrl, N, Ds, Acc) ->
if Depth == 1 -> nepp2_nl(Rest, ModAsStr, Hrl, N+1, Ds, Acc);
Depth > 1 -> nepp2_skip(Rest, Depth-1, ModAsStr, Hrl, N+1, Ds, Acc)
end;
nepp2_skip(<<"-ifdef", Rest/binary>>, Depth, ModAsStr, Hrl, N, Ds, Acc) ->
{_, Rest2} = read_until(Rest, "\n", ""),
nepp2_skip(Rest2, Depth+1, ModAsStr, Hrl, N+1, Ds, Acc);
nepp2_skip(<<"-ifndef", Rest/binary>>, Depth, ModAsStr, Hrl, N, Ds, Acc) ->
{_, Rest2} = read_until(Rest, "\n", ""),
nepp2_skip(Rest2, Depth+1, ModAsStr, Hrl, N+1, Ds, Acc);
nepp2_skip(<<$\n, Rest/binary>>, Depth, ModAsStr, Hrl, N, Ds, Acc) ->
nepp2_skip(Rest, Depth, ModAsStr, Hrl, N+1, Ds, Acc);
nepp2_skip(<<_, Rest/binary>>, Depth, ModAsStr, Hrl, N, Ds, Acc) ->
nepp2_skip(Rest, Depth, ModAsStr, Hrl, N, Ds, Acc).
read_until(<<C, Rest/binary>>, Delims, Acc) ->
case lists:member(C, Delims) of
true -> {lists:reverse(Acc), Rest};
false -> read_until(Rest, Delims, [C | Acc])
end.
parse_term(S) ->
{ok, Tokens, _End} = erl_scan:string(S),
{ok, Term} = erl_parse:parse_term(Tokens++[{dot,1}]),
Term.
classify_inc(F) ->
case lists:last(filename:split(F)) of
"gpb.hrl" -> gpb_hrl;
_ -> mod_hrl
end.
file_directive(File, N) ->
?ff("-file(\"~s\", ~p).\n", [File, N]).
split_toks_at_dot(AllToks) ->
case lists:splitwith(fun is_no_dot/1, AllToks) of
{Toks, [{dot,_}=Dot]} -> [Toks ++ [Dot]];
{Toks, [{dot,_}=Dot | Tl]} -> [Toks ++ [Dot] | split_toks_at_dot(Tl)]
end.
is_no_dot({dot,_}) -> false;
is_no_dot(_) -> true.
field_record_to_text() ->
record_to_text(?gpb_field, record_info(fields, ?gpb_field), #?gpb_field{}).
oneof_record_to_text() ->
record_to_text(gpb_oneof, record_info(fields, gpb_oneof), #gpb_oneof{}).
rpc_record_to_text() ->
record_to_text(?gpb_rpc, record_info(fields, ?gpb_rpc), #?gpb_rpc{}).
record_to_text(RecordName, Fields, DefaultR) ->
FieldTexts =
[if Default == undefined -> ?ff("~p", [FName]);
Default /= undefined -> ?ff("~p = ~p", [FName, Default])
end
|| {FName,Default} <- lists:zip(Fields, tl(tuple_to_list(DefaultR)))],
?f("-record(~p, {~s}).~n",
[RecordName, gpb_lib:comma_join(FieldTexts)]).
combine_erl_and_possible_nif(ErlCompilationResult, '$not_generated'=_Nif) ->
ErlCompilationResult;
combine_erl_and_possible_nif({ok, ModuleName, ErlCode}, NifTxt) ->
{ok, ModuleName, combine_erlcode_with_niftxt(ErlCode, NifTxt)};
combine_erl_and_possible_nif({ok, ModuleName, ErlCode, Warnings}, NifTxt) ->
{ok, ModuleName, combine_erlcode_with_niftxt(ErlCode, NifTxt), Warnings};
combine_erl_and_possible_nif(Error, _NifTxt) ->
Error.
combine_erlcode_with_niftxt(ErlCode, NifTxt) ->
[{erl, ErlCode},
{nif, NifTxt}].
%% -- internal utilities -----------------------------------------------------
flatten_iolist(IoList) ->
binary_to_list(iolist_to_binary(IoList)).
file_read_file(FileName, Opts) ->
file_op(read_file, [FileName], Opts).
file_read_file_info(FileName, Opts) ->
file_op(read_file_info, [FileName], Opts).
file_write_file(FileName, Bin, Opts) ->
file_op(write_file, [FileName, Bin], Opts).
possibly_write_file(FileName, Bin, Opts) when is_binary(Bin) ->
file_op(write_file, [FileName, Bin], Opts);
possibly_write_file(_FileName, '$not_generated', _Opts) ->
ok.
file_op(FnName, Args, Opts) ->
case proplists:get_value(file_op, Opts) of
undefined ->
apply(file, FnName, Args);
Ops ->
case proplists:get_value(FnName, Ops) of
undefined ->
apply(file, FnName, Args);
Fn ->
apply(Fn, Args)
end
end.
possibly_probe_defs(Defs, Opts) ->
case proplists:get_value(probe_defs, Opts, '$no') of
'$no' -> ok;
Fn -> Fn(Defs)
end.