@@ -16,14 +16,15 @@ defmodule Mix.Tasks.Rustler.New do
16
16
@ basic [
17
17
{ :eex , "basic/README.md" , "README.md" } ,
18
18
{ :eex , "basic/Cargo.toml.eex" , "Cargo.toml" } ,
19
- { :eex , "basic/src/lib.rs" , "src/lib.rs" } ,
20
- { :text , "basic/.gitignore" , ".gitignore" }
19
+ { :eex , "basic/src/lib.rs" , "src/lib.rs" }
21
20
]
22
21
23
22
@ root [
24
23
{ :eex , "root/Cargo.toml.eex" , "Cargo.toml" }
25
24
]
26
25
26
+ @ fallback_version "0.36.1"
27
+
27
28
root = Path . join ( :code . priv_dir ( :rustler ) , "templates/" )
28
29
29
30
for { format , source , _ } <- @ basic ++ @ root do
@@ -50,6 +51,8 @@ defmodule Mix.Tasks.Rustler.New do
50
51
module
51
52
end
52
53
54
+ module_as_atom = parse_module_name! ( module )
55
+
53
56
name =
54
57
case opts [ :name ] do
55
58
nil ->
@@ -69,41 +72,81 @@ defmodule Mix.Tasks.Rustler.New do
69
72
otp_app -> otp_app
70
73
end
71
74
72
- check_module_name_validity! ( module )
75
+ rustler_version = rustler_version ( )
73
76
74
77
path = Path . join ( [ File . cwd! ( ) , "native" , name ] )
75
- new ( otp_app , path , module , name , opts )
76
-
77
- copy_from ( File . cwd! ( ) , [ library_name: name ] , @ root )
78
+ new ( otp_app , path , module_as_atom , name , rustler_version , opts )
79
+
80
+ if Path . join ( File . cwd! ( ) , "Cargo.toml" ) |> File . exists? ( ) do
81
+ Mix . shell ( ) . info ( [
82
+ :green ,
83
+ "Workspace Cargo.toml already exists, please add " ,
84
+ :bright ,
85
+ path |> Path . relative_to_cwd ( ) ,
86
+ :reset ,
87
+ :green ,
88
+ " to the " ,
89
+ :bright ,
90
+ "\" members\" " ,
91
+ :reset ,
92
+ :green ,
93
+ " list"
94
+ ] )
95
+ else
96
+ copy_from ( File . cwd! ( ) , [ library_name: name ] , @ root )
97
+
98
+ gitignore = Path . join ( File . cwd! ( ) , ".gitignore" )
99
+
100
+ if gitignore |> File . exists? ( ) do
101
+ Mix . shell ( ) . info ( [ :green , "Updating .gitignore file" ] )
102
+ File . write ( gitignore , "\n # Rust binary artifacts\n /target/\n " , [ :append ] )
103
+ else
104
+ create_file ( gitignore , "/target/\n " )
105
+ end
106
+ end
78
107
79
- Mix.Shell.IO . info ( [ :green , "Ready to go! See #{ path } /README.md for further instructions." ] )
108
+ Mix . shell ( ) . info ( [
109
+ :green ,
110
+ "\n Ready to go! See #{ path |> Path . relative_to_cwd ( ) } /README.md for further instructions."
111
+ ] )
80
112
end
81
113
82
- defp new ( otp_app , path , module , name , _opts ) do
83
- module_elixir = "Elixir." <> module
84
-
114
+ defp new ( otp_app , path , module , name , rustler_version , _opts ) do
85
115
binding = [
86
116
otp_app: otp_app ,
87
- project_name: module_elixir ,
88
- native_module: module_elixir ,
89
- module: module ,
117
+ # Elixir syntax for the README
118
+ module: module |> Macro . to_string ( ) ,
119
+ # Erlang syntax for the init! invocation
120
+ native_module: module |> Atom . to_string ( ) ,
90
121
library_name: name ,
91
- rustler_version: Rustler . rustler_version ( )
122
+ rustler_version: rustler_version
92
123
]
93
124
94
125
copy_from ( path , binding , @ basic )
95
126
end
96
127
97
- defp check_module_name_validity! ( name ) do
98
- if ! ( name =~ ~r/ ^[A-Z]\w *(\. [A-Z]\w *)*$/ ) do
99
- Mix . raise (
100
- "Module name must be a valid Elixir alias (for example: Foo.Bar), got: #{ inspect ( name ) } "
101
- )
128
+ defp parse_module_name! ( name ) do
129
+ case Code . string_to_quoted ( name ) do
130
+ { :ok , atom } when is_atom ( atom ) ->
131
+ atom
132
+
133
+ { :ok , { :__aliases__ , _ , parts } } ->
134
+ Module . concat ( parts )
135
+
136
+ _ ->
137
+ Mix . raise (
138
+ "Module name must be a valid Elixir alias (for example: Foo.Bar, or :foo_bar), got: #{ inspect ( name ) } "
139
+ )
102
140
end
103
141
end
104
142
105
143
defp format_module_name_as_name ( module_name ) do
106
- String . replace ( String . downcase ( module_name ) , "." , "_" )
144
+ if module_name |> String . starts_with? ( ":" ) do
145
+ # Skip first
146
+ module_name |> String . downcase ( ) |> String . slice ( 1 .. - 1 // 1 )
147
+ else
148
+ module_name |> String . downcase ( ) |> String . replace ( "." , "_" )
149
+ end
107
150
end
108
151
109
152
defp copy_from ( target_dir , binding , mapping ) when is_list ( mapping ) do
@@ -134,9 +177,66 @@ defmodule Mix.Tasks.Rustler.New do
134
177
end
135
178
136
179
defp prompt ( message ) do
137
- Mix.Shell.IO . print_app ( )
180
+ Mix . shell ( ) . print_app ( )
138
181
resp = IO . gets ( IO.ANSI . format ( [ message , :white , " > " ] ) )
139
182
?\n = :binary . last ( resp )
140
183
:binary . part ( resp , { 0 , byte_size ( resp ) - 1 } )
141
184
end
185
+
186
+ @ doc false
187
+ defp rustler_version do
188
+ versions =
189
+ case Mix.Utils . read_path ( "https://crates.io/api/v1/crates/rustler" ,
190
+ timeout: 10_000 ,
191
+ unsafe_uri: true
192
+ ) do
193
+ { :ok , body } ->
194
+ get_versions ( body )
195
+
196
+ err ->
197
+ raise err
198
+ end
199
+
200
+ try do
201
+ result =
202
+ versions
203
+ |> Enum . map ( & Version . parse! / 1 )
204
+ |> Enum . filter ( & ( & 1 . pre == [ ] ) )
205
+ |> Enum . max ( Version )
206
+ |> Version . to_string ( )
207
+
208
+ Mix . shell ( ) . info ( "Fetched latest rustler crate version: #{ result } " )
209
+ result
210
+ rescue
211
+ ex ->
212
+ Mix . shell ( ) . error (
213
+ "Failed to fetch rustler crate versions, using hardcoded fallback: #{ @ fallback_version } \n Error: #{ ex |> Kernel . inspect ( ) } "
214
+ )
215
+
216
+ @ fallback_version
217
+ end
218
+ end
219
+
220
+ defp get_versions ( data ) do
221
+ cond do
222
+ # Erlang 27
223
+ Code . ensure_loaded? ( :json ) and Kernel . function_exported? ( :json , :decode , 1 ) ->
224
+ data |> :json . decode ( ) |> versions_from_parsed_json ( )
225
+
226
+ Code . ensure_loaded? ( Jason ) ->
227
+ data |> Jason . decode! ( ) |> versions_from_parsed_json ( )
228
+
229
+ true ->
230
+ # Nasty hack: Instead of parsing the JSON, we use a regex, abusing the
231
+ # compact nature of the returned data
232
+ Regex . scan ( ~r/ "num":"([^"]+)"/ , data ) |> Enum . map ( fn [ _ , res ] -> res end )
233
+ end
234
+ end
235
+
236
+ defp versions_from_parsed_json ( parsed ) do
237
+ parsed
238
+ |> Map . fetch! ( "versions" )
239
+ |> Enum . filter ( fn version -> not version [ "yanked" ] end )
240
+ |> Enum . map ( fn version -> version [ "num" ] end )
241
+ end
142
242
end
0 commit comments