Repository: comonicon/Comonicon.jl
Pull request #156 (2021-11-29 06:01:41)

support shell installation

codecov[bot]: # [Codecov](https://codecov.io/gh/comonicon/Comonicon.jl/pull/156?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) Report
> Merging [#156](https://codecov.io/gh/comonicon/Comonicon.jl/pull/156?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) (543c591) into [master](https://codecov.io/gh/comonicon/Comonicon.jl/commit/d5072d73011cfafb08df8bd0c81fcc4005e51158?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) (d5072d7) will **decrease** coverage by `61.95%`.
> The diff coverage is `0.00%`.

[![Impacted file tree graph](https://codecov.io/gh/comonicon/Comonicon.jl/pull/156/graphs/tree.svg?width=650&height=150&src=pr&token=zZjCxCiFTY&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None)](https://codecov.io/gh/comonicon/Comonicon.jl/pull/156?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None)

```diff
@@            Coverage Diff             @@
##           master    #156       +/-   ##
==========================================
- Coverage   63.57%   1.61%   -61.96%     
==========================================
  Files          16      17        +1     
  Lines        1142    1173       +31     
==========================================
- Hits          726      19      -707     
- Misses        416    1154      +738     
```


| [Impacted Files](https://codecov.io/gh/comonicon/Comonicon.jl/pull/156?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) | Coverage Δ | |
|---|---|---|
| [src/builder/install.jl](https://codecov.io/gh/comonicon/Comonicon.jl/pull/156/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None#diff-c3JjL2J1aWxkZXIvaW5zdGFsbC5qbA==) | `0.00% <0.00%> (ø)` | |
| [src/builder/rcfile.jl](https://codecov.io/gh/comonicon/Comonicon.jl/pull/156/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None#diff-c3JjL2J1aWxkZXIvcmNmaWxlLmps) | `0.00% <0.00%> (ø)` | |
| [src/tools.jl](https://codecov.io/gh/comonicon/Comonicon.jl/pull/156/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None#diff-c3JjL3Rvb2xzLmps) | `0.00% <0.00%> (ø)` | |
| [src/compat.jl](https://codecov.io/gh/comonicon/Comonicon.jl/pull/156/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None#diff-c3JjL2NvbXBhdC5qbA==) | `0.00% <0.00%> (-100.00%)` | :arrow_down: |
| [src/frontend/markdown.jl](https://codecov.io/gh/comonicon/Comonicon.jl/pull/156/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None#diff-c3JjL2Zyb250ZW5kL21hcmtkb3duLmps) | `0.00% <0.00%> (-90.84%)` | :arrow_down: |
| [src/codegen/zsh.jl](https://codecov.io/gh/comonicon/Comonicon.jl/pull/156/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None#diff-c3JjL2NvZGVnZW4venNoLmps) | `0.00% <0.00%> (-89.29%)` | :arrow_down: |
| [src/codegen/julia.jl](https://codecov.io/gh/comonicon/Comonicon.jl/pull/156/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None#diff-c3JjL2NvZGVnZW4vanVsaWEuamw=) | `0.00% <0.00%> (-87.44%)` | :arrow_down: |
| [src/ast/printing.jl](https://codecov.io/gh/comonicon/Comonicon.jl/pull/156/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None#diff-c3JjL2FzdC9wcmludGluZy5qbA==) | `0.00% <0.00%> (-78.35%)` | :arrow_down: |
| [src/frontend/cast.jl](https://codecov.io/gh/comonicon/Comonicon.jl/pull/156/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None#diff-c3JjL2Zyb250ZW5kL2Nhc3Quamw=) | `0.00% <0.00%> (-75.94%)` | :arrow_down: |
| [src/ast/utils.jl](https://codecov.io/gh/comonicon/Comonicon.jl/pull/156/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None#diff-c3JjL2FzdC91dGlscy5qbA==) | `15.00% <0.00%> (-72.50%)` | :arrow_down: |
| ... and [4 more](https://codecov.io/gh/comonicon/Comonicon.jl/pull/156/diff?src=pr&el=tree-more&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/comonicon/Comonicon.jl/pull/156?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/comonicon/Comonicon.jl/pull/156?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None). Last update [d5072d7...543c591](https://codecov.io/gh/comonicon/Comonicon.jl/pull/156?src=pr&el=lastupdated&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None).


Patch:
diff --git a/src/Comonicon.jl b/src/Comonicon.jl
index 0c1148b..456e9df 100644
--- a/src/Comonicon.jl
+++ b/src/Comonicon.jl
@@ -22,6 +22,7 @@ include("options.jl")
 include("ast/ast.jl")
 include("codegen/julia.jl")
 include("codegen/zsh.jl")
+include("tools.jl")
 
 include("frontend/frontend.jl")
 include("builder/builder.jl")
diff --git a/src/builder/builder.jl b/src/builder/builder.jl
index 083854a..3f794c3 100644
--- a/src/builder/builder.jl
+++ b/src/builder/builder.jl
@@ -2,7 +2,7 @@ module Builder
 
 using ..Options: Options
 using ..ZSHCompletions
-using ..Comonicon: get_version
+using ..Comonicon: get_version, Tools
 using PackageCompiler
 using Logging
 using TOML: TOML
@@ -18,5 +18,6 @@ include("install.jl")
 include("sysimg.jl")
 include("app.jl")
 include("tarball.jl")
+include("rcfile.jl")
 
 end
diff --git a/src/builder/install.jl b/src/builder/install.jl
index ddf8806..2b664da 100644
--- a/src/builder/install.jl
+++ b/src/builder/install.jl
@@ -8,12 +8,15 @@ function install(m::Module, options::Options.Comonicon)
     install_project_env(m, options)
     # prepare .julia/bin/<command>
     install_entryfile(m, options)
-    # generate auto completion script
-    # at .julia/completions
-    install_completion(m, options)
     # download or build sysimg if configured
     # at .julia/scratchspaces/<uuid>/sysimg/lib<name>.dylib
     install_sysimg(m, options)
+
+    haskey(ENV, "SHELL") || return
+    shell = basename(ENV["SHELL"])
+    # generate auto completion script
+    # at .julia/completions
+    install_completion(m, options, shell)
     return
 end
 
@@ -25,18 +28,6 @@ function install_project_env(m::Module, options::Options.Comonicon)
     return
 end
 
-function install_completion(m::Module, options::Options.Comonicon)
-    haskey(ENV, "SHELL") || return
-    shell = basename(ENV["SHELL"])
-    completions_dir = install_path(options, "completions")
-    completion_file = joinpath(completions_dir, "_" * options.name)
-    shell in ["zsh"] || return # only emit supported shell
-    open(completion_file, "w+") do io
-        print(io, completion_script(m, options, shell))
-    end
-    return
-end
-
 function install_entryfile(m::Module, options::Options.Comonicon)
     bin = install_path(options, "bin")
     # generate entry file at .juila/bin
@@ -74,6 +65,16 @@ function install_sysimg(m::Module, options::Options.Comonicon)
     return
 end
 
+function install_completion(m::Module, options::Options.Comonicon, shell::String)
+    completions_dir = install_path(options, "completions")
+    completion_file = joinpath(completions_dir, "_" * options.name)
+    shell in ["zsh"] || return # only emit supported shell
+    open(completion_file, "w+") do io
+        print(io, completion_script(m, options, shell))
+    end
+    return
+end
+
 function completion_script(m::Module, options::Options.Comonicon, shell::String)
     isdefined(m, :CASTED_COMMANDS) || error("cannot find Comonicon CLI entry")
     haskey(m.CASTED_COMMANDS, "main") || error("cannot find Comonicon CLI entry")
diff --git a/src/builder/rcfile.jl b/src/builder/rcfile.jl
new file mode 100644
index 0000000..f6ed2c3
--- /dev/null
+++ b/src/builder/rcfile.jl
@@ -0,0 +1,85 @@
+function install_env_path(m::Module, options::Options.Comonicon = Options.read_options(m);
+        shell::String = basename(ENV["SHELL"]), home_dir=homedir(), env=ENV, yes::Bool = false)
+
+    rcfile = detect_rcfile(shell, home_dir)
+    install_path = expanduser(options.install.path)
+    msg = "cannot detect $(options.install.path)/bin in PATH, do you want to add it in PATH?"
+    if !contains_path(rcfile, install_path, env) && Tools.prompt(msg, yes)
+        write_path(rcfile, install_path)
+    end
+
+    msg = "cannot detect $(options.install.path)/completions in FPATH, do you want to add it in FPATH?"
+    if !contains_fpath(rcfile, install_path, env) && Tools.prompt(msg, yes)
+        write_fpath(rcfile, install_path)
+    end
+
+    @info "open a new terminal, or source $rcfile to enable the new PATH."
+    return
+end
+
+function contains_path(rcfile::String, path::String, env = ENV)
+    bin = joinpath(path, "bin")
+    return contains_shell_var(rcfile, "PATH", bin, env)
+end
+
+function contains_fpath(rcfile::String, path::String, env = ENV)
+    completions = joinpath(path, "completions")
+    return contains_shell_var(rcfile, "FPATH", completions, env)
+end
+
+function write_path(rcfile::String, path::String)
+    bin = joinpath(path, "bin")
+    return export_shell_var(rcfile, "PATH", bin)
+end
+
+function write_fpath(rcfile::String, path::String)
+    completions = joinpath(path, "completions")
+    return export_shell_var(rcfile, "FPATH", completions)
+end
+
+function contains_shell_var(rcfile, var, value, env)
+    if !haskey(env, var)
+        for line in readlines(rcfile)
+            if strip(line) == "export $var=\"$(relhome(value)):\$$var\"" ||
+               strip(line) == "export $var=\"$value:\$$var\""
+                return true
+            end
+        end
+        return false
+    end
+
+    for each in split(env[var], ":")
+        (each == relhome(value) || each == value) && return true
+    end
+    return false    
+end
+
+function export_shell_var(rcfile, var, path)
+    isempty(rcfile) && return
+    script = """
+    # generated by Comonicon
+    export $var="$(relhome(path)):\$$var"
+    """
+    open(rcfile, "a") do io
+        write(io, "\n" * script * "\n")
+    end
+end
+
+relhome(path) = joinpath("\$HOME", relpath(path, homedir()))
+
+function detect_rcfile(shell::String=basename(ENV["SHELL"]), home = homedir())
+    if shell == "zsh"
+        rcfile = joinpath((haskey(ENV, "ZDOTDIR") ? ENV["ZDOTDIR"] : home), ".zshrc")
+    elseif shell == "bash"
+        rcfile = joinpath(home, ".bashrc")
+        if !isfile(rcfile)
+            rcfile = joinpath(home, ".bash_profile")
+        end
+        return rcfile
+    else
+        error(
+            "auto installation for $shell is not supported, " *
+            "please open an issue under $COMONICON_URL"
+        )
+    end
+end
diff --git a/src/frontend/cast.jl b/src/frontend/cast.jl
index d0e57ff..d8b5616 100644
--- a/src/frontend/cast.jl
+++ b/src/frontend/cast.jl
@@ -213,15 +213,23 @@ function codegen_project_entry(m::Module, line, @nospecialize(ex))
         """
         comonicon_install(; kwargs...) = $Comonicon.Builder.install($m; kwargs...)
 
+        """
+            comonicon_build(; kw...)
+
+        Launch the Comonicon build CLI. This is usually used in the `deps/build.jl`
+        to install the CLI while installing the package.
+        """
         comonicon_build(; kw...) = $Comonicon.Builder.command_main($m; kw...)
 
         """
-            comonicon_install_path([quiet=false])
-        Install the `PATH` and `FPATH` to your shell configuration file. You can use `comonicon_install_path(true)`,
-        to skip interactive prompt.
-        For more detailed reference, please refer to [Comonicon documentation](https://docs.comonicon.org).
+            comonicon_install_path(;[yes=false])
+
+        Install the `PATH` and `FPATH` to your shell configuration file.
+        You can use `comonicon_install_path(;yes=true)` to skip interactive prompt.
+        For more detailed reference, please refer to
+        [Comonicon documentation](https://docs.comonicon.org).
         """
-        comonicon_install_path() = $Comonicon.Builder.install_env_path()
+        comonicon_install_path(;yes=false) = $Comonicon.Builder.install_env_path($m;yes)
 
         precompile(Tuple{typeof($m.command_main),Array{String,1}})
     end
diff --git a/src/tools.jl b/src/tools.jl
new file mode 100644
index 0000000..e1d7741
--- /dev/null
+++ b/src/tools.jl
@@ -0,0 +1,22 @@
+module Tools
+
+prompt(msg, yes::Bool = false) = prompt(stdin, msg, yes)
+
+function prompt(io::IO, msg, yes::Bool = false)
+    print(msg)
+
+    if yes
+        println(" Yes.")
+    else
+        print(" [Y/n] ")
+        run(`stty raw`)
+        input = read(io, Char)
+        run(`stty cooked`)
+        println()
+        input in ['Y', 'y', '\n', '\r'] || return false
+    end
+
+    return true
+end
+
+end
\ No newline at end of file


Test patch:
diff --git a/test/Project.toml b/test/Project.toml
index a7385e7..7a3f939 100644
--- a/test/Project.toml
+++ b/test/Project.toml
@@ -9,3 +9,4 @@ Pkg = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f"
 Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c"
 SimpleMock = "a896ed2c-15a5-4479-b61d-a0e88e2a1d25"
 Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"
+Scratch = "6c6a2e73-6563-6170-7368-637461726353"
diff --git a/test/builder/cli.jl b/test/builder/cli.jl
index fc330a3..f51239e 100644
--- a/test/builder/cli.jl
+++ b/test/builder/cli.jl
@@ -1,3 +1,5 @@
+module TestBuilderCLI
+
 using Test
 using Comonicon.Options
 using Comonicon.Builder: print_builder_help, command_main
@@ -17,3 +19,5 @@ module TestCLI end
         command_main(TestCLI, options)
     end === nothing
 end
+
+end
diff --git a/test/builder/install.jl b/test/builder/install.jl
index 100b6be..9390b0a 100644
--- a/test/builder/install.jl
+++ b/test/builder/install.jl
@@ -1,7 +1,10 @@
+module TestBuilderInstall
+
 using Test
 using Scratch
 using Comonicon.Options
-using Comonicon.Builder: ensure_path, detect_shell, entryfile_script, completion_script
+using Comonicon.Builder: ensure_path, entryfile_script,
+    completion_script, detect_rcfile
 
 @testset "ensure_path" begin
     path = tempname()
@@ -10,16 +13,6 @@ using Comonicon.Builder: ensure_path, detect_shell, entryfile_script, completion
     @test ispath(path) == true    
 end
 
-@testset "detect_shell" begin
-    withenv("SHELL"=>"/bin/bash") do
-        @test detect_shell() == "bash"
-    end
-
-    withenv("SHELL"=>"/bin/zsh") do
-        @test detect_shell() == "zsh"
-    end
-end
-
 module TestInstall
 
 using Comonicon
@@ -44,16 +37,29 @@ end
     @test occursin("--compile=yes \\\n", script)
     @test occursin("--optimize=2 \\\n", script)
     @test occursin("-- \"\${BASH_SOURCE[0]}\"", script)
-    @test occursin("using Main.TestInstall\nexit(Main.TestInstall.command_main())", script)        
+    @test occursin("using Main.TestBuilderInstall.TestInstall\nexit(Main.TestBuilderInstall.TestInstall.command_main())", script)        
 end
 
 @testset "test completion script" begin
+    options = Options.Comonicon(name="test")
     withenv("SHELL"=>"/bin/zsh") do
-        script = completion_script(TestInstall, options)
+        script = completion_script(TestInstall, options, "zsh")
         @test occursin("#compdef _testinstall testinstall \n", script)
     end
 
     withenv("SHELL"=>"/bin/fakesh") do
-        @test_throws ErrorException completion_script(TestInstall, options)
+        @test_throws ErrorException completion_script(TestInstall, options, "/bin/fakesh")
     end
 end
+
+@testset "detect_rcfile" begin
+    withenv("SHELL" => "zsh") do
+        @test detect_rcfile("zsh") == joinpath(homedir(), ".zshrc")
+    end
+
+    withenv("SHELL" => "zsh", "ZDOTDIR" => "zsh_dir") do
+        @test detect_rcfile("zsh") == joinpath("zsh_dir", ".zshrc")
+    end
+end
+
+end # TestBuilderInstall
\ No newline at end of file
diff --git a/test/builder/rcfile.jl b/test/builder/rcfile.jl
new file mode 100644
index 0000000..b300d0d
--- /dev/null
+++ b/test/builder/rcfile.jl
@@ -0,0 +1,64 @@
+module TestBuilderRCFile
+
+using Test
+using Comonicon
+using Comonicon.Builder: contains_path, contains_fpath,
+    write_path, write_fpath, detect_rcfile, install_env_path
+
+home_dir = mktempdir()
+test_dir = pkgdir(Comonicon, "test")
+usr_dir = joinpath(test_dir, "usr")
+
+options = Comonicon.Options.Comonicon(
+    name="test",
+    install=Comonicon.Options.Install(
+        path=usr_dir,
+        completion=false,
+    ),
+    sysimg=nothing,
+)
+
+@testset "contains path/write path" begin
+    withenv("PATH"=>nothing, "FPATH"=>nothing) do
+        dir = mktempdir()
+        rcfile = joinpath(dir, ".bashrc")
+        touch(rcfile)
+        @test contains_path(rcfile, usr_dir, Base.EnvDict()) == false
+
+        write_path(rcfile, usr_dir)
+        @test contains_path(rcfile, usr_dir) == true
+
+        @test contains_fpath(rcfile, usr_dir, Base.EnvDict()) == false
+
+        write_fpath(rcfile, usr_dir)
+        @test contains_fpath(rcfile, usr_dir) == true
+    end
+
+    withenv("PATH"=>joinpath(usr_dir, "bin"), "FPATH"=>joinpath(usr_dir, "completions")) do
+        @test contains_path("rcfile", usr_dir) == true
+        @test contains_fpath("rcfile", usr_dir) == true
+    end
+end
+
+@testset "detect rcfile" begin
+    @test detect_rcfile("zsh", home_dir) == joinpath(home_dir, ".zshrc")
+    @test withenv("ZDOTDIR"=>"test") do
+        detect_rcfile("zsh", home_dir)
+    end == joinpath("test", ".zshrc")
+
+    @test detect_rcfile("bash", home_dir) == joinpath(home_dir, ".bash_profile")
+    touch(joinpath(home_dir, ".bashrc"))
+    @test detect_rcfile("bash", home_dir) == joinpath(home_dir, ".bashrc")
+end
+
+@testset "install env path" begin
+    install_env_path(Main, options; shell="zsh", home_dir, env=ENV, yes=true)
+
+    withenv("PATH"=>nothing, "FPATH"=>nothing) do
+        @test contains_path(joinpath(home_dir, ".zshrc"), usr_dir)
+        @test contains_fpath(joinpath(home_dir, ".zshrc"), usr_dir)
+    end
+end
+
+
+end # TestBuilderRCFile
diff --git a/test/builder/tarball.jl b/test/builder/tarball.jl
deleted file mode 100644
index 1900f60..0000000
--- a/test/builder/tarball.jl
+++ /dev/null
@@ -1,4 +0,0 @@
-using Test
-using Comonicon.Builder: tarball_name
-
-tarball_name(TestCLI, "test")
\ No newline at end of file
diff --git a/test/runtests.jl b/test/runtests.jl
index 770fabc..46f89b5 100644
--- a/test/runtests.jl
+++ b/test/runtests.jl
@@ -23,6 +23,11 @@ end
     include("codegen/codegen.jl")
 end
 
+@testset "builder" begin
+    include("builder/install.jl")
+    include("builder/cli.jl")
+end
+
 @testset "scripts" begin
     include("scripts.jl")
 end