@@ -61,9 +61,20 @@ defmodule ElixirLS.Mix do
6161
6262 @ mix_install_project Mix.InstallProject
6363
64- # This is a forked version of https://github.com/elixir-lang/elixir/blob/c521bdb91a77b36be16fdf18d632ad7719de4f91/lib/mix/lib/mix.ex#L765
65- # with added option to disable stopping apps after install
66- # we don't want hex app stopped
64+ # This is a forked version of Mix.install from Elixir's lib/mix/lib/mix.ex
65+ # Originally forked from commit c521bdb91a77b36be16fdf18d632ad7719de4f91
66+ # Updated with upstream changes through the current Elixir version
67+ # Includes custom option :stop_started_applications to prevent stopping hex app
68+ #
69+ # Backward compatibility: Supports Elixir 1.13+
70+ # Features gated by Version.match? checks:
71+ # - Keyword.validate! for option validation (Elixir 1.13+, uses function_exported?)
72+ # - Mix.Dep.Lock.read/1 and write/2 for external lockfile (Elixir 1.14+)
73+ # - Mix.Task.clear/0 for task cleanup (Elixir 1.14+)
74+ # - MIX_INSTALL_RESTORE_PROJECT_DIR support (Elixir 1.16.2+)
75+ # - Shorter install directory paths ex-X-erl-Y (Elixir 1.19+)
76+ # - Cache ID encoding with url_encode64 (Elixir 1.19+)
77+ #
6778 # The original code is licensed under
6879
6980 # Apache License
@@ -277,71 +288,112 @@ defmodule ElixirLS.Mix do
277288 other
278289 end )
279290
280- config = Keyword . get ( opts , :config , [ ] )
291+ # Use Keyword.validate! if available (Elixir 1.13+), otherwise manual validation
292+ opts =
293+ if function_exported? ( Keyword , :validate! , 2 ) do
294+ Keyword . validate! ( opts ,
295+ config: [ ] ,
296+ config_path: nil ,
297+ consolidate_protocols: true ,
298+ compilers: [ :elixir ] ,
299+ elixir: nil ,
300+ force: false ,
301+ lockfile: nil ,
302+ runtime_config: [ ] ,
303+ start_applications: true ,
304+ # custom elixirLS option
305+ stop_started_applications: true ,
306+ system_env: [ ] ,
307+ verbose: false
308+ )
309+ else
310+ # Fallback for older Elixir versions
311+ opts
312+ end
313+
281314 config_path = expand_path ( opts [ :config_path ] , deps , :config_path , "config/config.exs" )
315+ config = Keyword . get ( opts , :config , [ ] )
282316 system_env = Keyword . get ( opts , :system_env , [ ] )
283317 consolidate_protocols? = Keyword . get ( opts , :consolidate_protocols , true )
284318 start_applications? = Keyword . get ( opts , :start_applications , true )
319+ compilers = Keyword . get ( opts , :compilers , [ :elixir ] )
285320 # custom elixirLS option
286321 stop_started_applications? = Keyword . get ( opts , :stop_started_applications , true )
287322
288323 id =
289- { deps , config , system_env , consolidate_protocols? }
290- |> :erlang . term_to_binary ( )
291- |> :erlang . md5 ( )
292- |> Base . encode16 ( case: :lower )
324+ if Version . match? ( System . version ( ) , ">= 1.19.0-dev" ) do
325+ { deps , config , system_env , consolidate_protocols? }
326+ |> :erlang . term_to_binary ( )
327+ |> :erlang . md5 ( )
328+ |> Base . url_encode64 ( padding: false )
329+ else
330+ { deps , config , system_env , consolidate_protocols? }
331+ |> :erlang . term_to_binary ( )
332+ |> :erlang . md5 ( )
333+ |> Base . encode16 ( case: :lower )
334+ end
293335
294- force? = System . get_env ( "MIX_INSTALL_FORCE" ) in [ "1" , "true" ] or ! ! opts [ :force ]
336+ force? =
337+ System . get_env ( "MIX_INSTALL_FORCE" ) in [ "1" , "true" ] or Keyword . get ( opts , :force , false )
295338
296339 case Mix.State . get ( :installed ) do
297340 nil ->
298- Application . put_all_env ( config , persistent: true )
299341 System . put_env ( system_env )
342+ install_project_dir = install_dir ( id )
300343
301- install_dir = install_dir ( id )
302-
303- if opts [ :verbose ] do
304- Mix . shell ( ) . info ( "Mix.install/2 using #{ install_dir } " )
344+ if Keyword . get ( opts , :verbose , false ) do
345+ Mix . shell ( ) . info ( "Mix.install/2 using #{ install_project_dir } " )
305346 end
306347
307348 if force? do
308- File . rm_rf! ( install_dir )
349+ File . rm_rf! ( install_project_dir )
309350 end
310351
311- config = [
312- version: "0.1.0" ,
313- build_embedded: false ,
314- build_per_environment: true ,
315- build_path: "_build" ,
316- lockfile: "mix.lock" ,
317- deps_path: "deps" ,
352+ dynamic_config = [
318353 deps: deps ,
319- app: :mix_install ,
320- erlc_paths: [ ] ,
321- elixirc_paths: [ ] ,
322- compilers: [ ] ,
323354 consolidate_protocols: consolidate_protocols? ,
324355 config_path: config_path ,
325- prune_code_paths: false
356+ compilers: compilers
326357 ]
327358
359+ :ok =
360+ Mix.ProjectStack . push (
361+ @ mix_install_project ,
362+ [ compile_config: config ] ++ install_project_config ( dynamic_config ) ,
363+ "nofile"
364+ )
365+
328366 started_apps = Application . started_applications ( )
329- :ok = Mix.ProjectStack . push ( @ mix_install_project , config , "nofile" )
330- build_dir = Path . join ( install_dir , "_build" )
367+ build_dir = Path . join ( install_project_dir , "_build" )
331368 external_lockfile = expand_path ( opts [ :lockfile ] , deps , :lockfile , "mix.lock" )
332369
333370 try do
334371 first_build? = not File . dir? ( build_dir )
335- File . mkdir_p! ( install_dir )
336372
337- File . cd! ( install_dir , fn ->
373+ # MIX_INSTALL_RESTORE_PROJECT_DIR support (Elixir 1.16.2+)
374+ restore_dir =
375+ if Version . match? ( System . version ( ) , ">= 1.16.2-dev" ) do
376+ System . get_env ( "MIX_INSTALL_RESTORE_PROJECT_DIR" )
377+ end
378+
379+ if first_build? and restore_dir != nil and not force? do
380+ File . cp_r ( restore_dir , install_project_dir )
381+ remove_dep ( install_project_dir , "mix_install" )
382+ end
383+
384+ File . mkdir_p! ( install_project_dir )
385+
386+ File . cd! ( install_project_dir , fn ->
387+ # This step needs to be mirrored in mix deps.partition
388+ Application . put_all_env ( config , persistent: true )
389+
338390 if config_path do
339391 Mix.Task . rerun ( "loadconfig" )
340392 end
341393
342394 cond do
343395 external_lockfile ->
344- md5_path = Path . join ( install_dir , "merge.lock.md5" )
396+ md5_path = Path . join ( install_project_dir , "merge.lock.md5" )
345397
346398 old_md5 =
347399 case File . read ( md5_path ) do
@@ -352,8 +404,9 @@ defmodule ElixirLS.Mix do
352404 new_md5 = external_lockfile |> File . read! ( ) |> :erlang . md5 ( )
353405
354406 if old_md5 != new_md5 do
407+ # Mix.Dep.Lock.read/1 and write/2 were added in Elixir 1.14
355408 if Version . match? ( System . version ( ) , ">= 1.14.0-dev" ) do
356- lockfile = Path . join ( install_dir , "mix.lock" )
409+ lockfile = Path . join ( install_project_dir , "mix.lock" )
357410 old_lock = Mix.Dep.Lock . read ( lockfile )
358411 new_lock = Mix.Dep.Lock . read ( external_lockfile )
359412 Mix.Dep.Lock . write ( Map . merge ( old_lock , new_lock ) , file: lockfile )
@@ -362,7 +415,8 @@ defmodule ElixirLS.Mix do
362415 else
363416 IO . puts (
364417 :stderr ,
365- "Lockfile conflict. Please clean up your mix install directory #{ install_dir } "
418+ "External lockfile support requires Elixir 1.14+. " <>
419+ "Please upgrade or remove the :lockfile option from Mix.install/2"
366420 )
367421
368422 System . halt ( 1 )
@@ -401,13 +455,24 @@ defmodule ElixirLS.Mix do
401455 end
402456 end
403457
404- Mix.State . put ( :installed , id )
458+ if restore_dir do
459+ remove_leftover_deps ( install_project_dir )
460+ end
461+
462+ Mix.State . put ( :installed , { id , dynamic_config } )
405463 :ok
406464 after
407465 Mix.ProjectStack . pop ( )
466+ # Clear all tasks invoked during installation, since there
467+ # is no reason to keep this in memory. Additionally this
468+ # allows us to rerun tasks for the dependencies later on,
469+ # such as recompilation (available in Elixir 1.14+)
470+ if Version . match? ( System . version ( ) , ">= 1.14.0-dev" ) do
471+ Mix.Task . clear ( )
472+ end
408473 end
409474
410- ^ id when not force? ->
475+ { ^ id , _dynamic_config } when not force? ->
411476 :ok
412477
413478 _ ->
@@ -460,12 +525,61 @@ defmodule ElixirLS.Mix do
460525 :ignore
461526 end
462527
528+ defp remove_leftover_deps ( install_project_dir ) do
529+ build_lib_dir = Path . join ( [ install_project_dir , "_build" , "dev" , "lib" ] )
530+
531+ deps = File . ls! ( build_lib_dir )
532+
533+ loaded_deps =
534+ for { app , _description , _version } <- Application . loaded_applications ( ) ,
535+ into: MapSet . new ( ) ,
536+ do: Atom . to_string ( app )
537+
538+ # We want to keep :mix_install, but it has no application
539+ loaded_deps = MapSet . put ( loaded_deps , "mix_install" )
540+
541+ for dep <- deps , not MapSet . member? ( loaded_deps , dep ) do
542+ remove_dep ( install_project_dir , dep )
543+ end
544+ end
545+
546+ defp remove_dep ( install_project_dir , dep ) do
547+ build_lib_dir = Path . join ( [ install_project_dir , "_build" , "dev" , "lib" ] )
548+ deps_dir = Path . join ( install_project_dir , "deps" )
549+
550+ build_path = Path . join ( build_lib_dir , dep )
551+ File . rm_rf ( build_path )
552+ dep_path = Path . join ( deps_dir , dep )
553+ File . rm_rf ( dep_path )
554+ end
555+
556+ defp install_project_config ( dynamic_config ) do
557+ [
558+ version: "1.0.0" ,
559+ build_per_environment: true ,
560+ build_path: "_build" ,
561+ lockfile: "mix.lock" ,
562+ deps_path: "deps" ,
563+ app: :mix_install ,
564+ erlc_paths: [ ] ,
565+ elixirc_paths: [ ] ,
566+ prune_code_paths: false
567+ ] ++ dynamic_config
568+ end
569+
463570 defp install_dir ( cache_id ) do
464571 install_root =
465572 System . get_env ( "MIX_INSTALL_DIR" ) ||
466573 Path . join ( Mix.Utils . mix_cache ( ) , "installs" )
467574
468- version = "elixir-#{ System . version ( ) } -erts-#{ :erlang . system_info ( :version ) } "
575+ # Use shorter path format in Elixir 1.19+
576+ version =
577+ if Version . match? ( System . version ( ) , ">= 1.19.0-dev" ) do
578+ "ex-#{ System . version ( ) } -erl-#{ :erlang . system_info ( :version ) } "
579+ else
580+ "elixir-#{ System . version ( ) } -erts-#{ :erlang . system_info ( :version ) } "
581+ end
582+
469583 Path . join ( [ install_root , version , cache_id ] )
470584 end
471585
0 commit comments