From 2934dc42c8d1483272708956d01715e0505f4f8a Mon Sep 17 00:00:00 2001 From: David Allsopp Date: Tue, 20 Oct 2015 17:08:05 +0100 Subject: [PATCH] Support Windows Command Processor as a shell Support cmd as an additional option to --shell. Additionally, use parent_putenv so that opam config env does not have to be run at the end of opam init or after opam switch. Windows Command Processor has no equivalent to .profile - at present, the user must run opam env every time they launch a command prompt. Signed-off-by: David Allsopp --- src/client/opamArg.ml | 3 +- src/client/opamClient.ml | 5 +- src/client/opamCommands.ml | 10 +- src/client/opamConfigCommand.ml | 17 ++- src/client/opamConfigCommand.mli | 11 +- src/core/opamStd.ml | 12 +- src/core/opamStd.mli | 4 +- src/format/opamTypes.mli | 2 +- src/format/opamTypesBase.ml | 1 + src/state/opamEnv.ml | 212 +++++++++++++++++++++---------- src/state/opamEnv.mli | 3 + 11 files changed, 194 insertions(+), 86 deletions(-) diff --git a/src/client/opamArg.ml b/src/client/opamArg.ml index a262884b449..5a5828f5f6f 100644 --- a/src/client/opamArg.ml +++ b/src/client/opamArg.ml @@ -738,12 +738,13 @@ let installed_roots_flag = mk_flag ["installed-roots"] "Display only the installed roots." let shell_opt = - let enum = [ + let (enum : (string * OpamTypes.shell) list) = [ "bash",`bash; "sh",`sh; "csh",`csh; "zsh",`zsh; "fish",`fish; + "cmd",`cmd; ] in mk_opt ["shell"] "SHELL" (Printf.sprintf diff --git a/src/client/opamClient.ml b/src/client/opamClient.ml index d03407863b0..f71635352c5 100644 --- a/src/client/opamClient.ml +++ b/src/client/opamClient.ml @@ -599,7 +599,10 @@ let init else false in let advised_deps = - [OpamStateConfig.(Lazy.force !r.makecmd); "m4"; "cc"] + if OpamStd.Sys.(os () = Win32) then + [] + else + [OpamStateConfig.(Lazy.force !r.makecmd); "m4"; "cc"] in (match List.filter (not @* check_external_dep) advised_deps with | [] -> () diff --git a/src/client/opamCommands.ml b/src/client/opamCommands.ml index 246b2cb3cbe..7dab1e583ea 100644 --- a/src/client/opamCommands.ml +++ b/src/client/opamCommands.ml @@ -859,10 +859,10 @@ let config = if OpamStateConfig.(!r.current_switch) = None then `Ok () else OpamSwitchState.with_ `Lock_none gt @@ fun st -> `Ok (OpamConfigCommand.env st - ~csh:(shell=`csh) ~sexp ~fish:(shell=`fish) ~inplace_path) + ~cmd:(shell=`cmd) ~csh:(shell=`csh) ~sexp ~fish:(shell=`fish) ~inplace_path) | Some `revert_env, [] -> `Ok (OpamConfigCommand.print_eval_env - ~csh:(shell=`csh) ~sexp ~fish:(shell=`fish) + ~cmd:(shell=`cmd) ~csh:(shell=`csh) ~sexp ~fish:(shell=`fish) (OpamEnv.add [] [])) | Some `setup, [] -> let user = all || user in @@ -885,7 +885,7 @@ let config = Main options\n\ \ -l, --list %s\n\ \ -a, --all %s\n\ - \ --shell=\n\ + \ --shell=\n\ \ Configure assuming the given shell.\n\ \n\ User configuration\n\ @@ -1150,10 +1150,10 @@ let env = if OpamStateConfig.(!r.current_switch) <> None then OpamSwitchState.with_ `Lock_none gt @@ fun st -> OpamConfigCommand.env st - ~csh:(shell=`csh) ~sexp ~fish:(shell=`fish) ~inplace_path + ~cmd:(shell=`cmd) ~csh:(shell=`csh) ~sexp ~fish:(shell=`fish) ~inplace_path | true -> OpamConfigCommand.print_eval_env - ~csh:(shell=`csh) ~sexp ~fish:(shell=`fish) + ~cmd:(shell=`cmd) ~csh:(shell=`csh) ~sexp ~fish:(shell=`fish) (OpamEnv.add [] []) in Term.(const env $global_options $shell_opt $sexp $inplace_path $revert), diff --git a/src/client/opamConfigCommand.ml b/src/client/opamConfigCommand.ml index 4c1339f5989..0a86a11d348 100644 --- a/src/client/opamConfigCommand.ml +++ b/src/client/opamConfigCommand.ml @@ -177,20 +177,31 @@ let rec print_fish_env env = k (OpamStd.Env.escape_single_quotes ~using_backslashes:true v)); print_fish_env r -let print_eval_env ~csh ~sexp ~fish env = +let print_cmd_env env = + List.iter (fun (k, v, _) -> OpamConsole.msg "set %s=%s\n" k v) env + +let print_eval_env ~cmd ~csh ~sexp ~fish env = if sexp then print_sexp_env env else if csh then print_csh_env env else if fish then print_fish_env env + else if cmd then + if OpamStd.Sys.tty_out then begin + log "parent-putenv"; + OpamEnv.set_cmd_env env + end else begin + log "cmd-stdout"; + print_cmd_env env + end else print_env env -let env st ~csh ~sexp ~fish ~inplace_path = +let env st ~cmd ~csh ~sexp ~fish ~inplace_path = log "config-env"; let env = OpamEnv.get_opam ~force_path:(not inplace_path) st in - print_eval_env ~csh ~sexp ~fish env + print_eval_env ~cmd ~csh ~sexp ~fish env let subst gt fs = log "config-substitute"; diff --git a/src/client/opamConfigCommand.mli b/src/client/opamConfigCommand.mli index 7090589ef13..84e61111979 100644 --- a/src/client/opamConfigCommand.mli +++ b/src/client/opamConfigCommand.mli @@ -14,17 +14,18 @@ open OpamTypes open OpamStateTypes -(** Display the current environment. Booleans csh, sexp and fish set an alternative - output (unspecified if more than one is true, sh-style by default). +(** Display the current environment. Booleans cmd, csh, sexp and fish set an + alternative output (unspecified if more than one is true, sh-style by + default). [inplace_path] changes how the PATH variable is updated when there is already an opam entry: either at the same rank, or pushed in front. *) val env: - 'a switch_state -> csh:bool -> sexp:bool -> fish:bool -> inplace_path:bool -> - unit + 'a switch_state -> cmd:bool -> csh:bool -> sexp:bool -> fish:bool -> + inplace_path:bool -> unit (** Like [env] but allows to specify the precise env to print rather than compute it from a switch state *) -val print_eval_env: csh:bool -> sexp:bool -> fish:bool -> env -> unit +val print_eval_env: cmd:bool -> csh:bool -> sexp:bool -> fish:bool -> env -> unit (** Display the content of all available variables; global summary if the list is empty, package name "-" is understood as global configuration *) diff --git a/src/core/opamStd.ml b/src/core/opamStd.ml index ffb02b94a8f..e346ef4a8df 100644 --- a/src/core/opamStd.ml +++ b/src/core/opamStd.ml @@ -888,7 +888,11 @@ module OpamSys = struct | "zsh" -> `zsh | "bash" -> `bash | "fish" -> `fish - | _ -> `sh + | _ -> + if os () = Win32 then + `cmd + else + `sh let executable_name = if os () = Win32 then @@ -902,7 +906,11 @@ module OpamSys = struct let guess_shell_compat () = try shell_of_string (Filename.basename (Env.get "SHELL")) - with Not_found -> `sh + with Not_found -> + if os () = Win32 then + `cmd + else + `sh let guess_dot_profile shell = let home f = diff --git a/src/core/opamStd.mli b/src/core/opamStd.mli index 7c787562e90..9a7ca5e6189 100644 --- a/src/core/opamStd.mli +++ b/src/core/opamStd.mli @@ -497,10 +497,10 @@ module Sys : sig val executable_name : string -> string (** Guess the shell compat-mode *) - val guess_shell_compat: unit -> [`csh|`zsh|`sh|`bash|`fish] + val guess_shell_compat: unit -> [`csh|`zsh|`sh|`bash|`fish|`cmd] (** Guess the location of .profile *) - val guess_dot_profile: [`csh|`zsh|`sh|`bash|`fish] -> string + val guess_dot_profile: [`csh|`zsh|`sh|`bash|`fish|`cmd] -> string (** The separator character used in the PATH variable (varies depending on OS) *) diff --git a/src/format/opamTypes.mli b/src/format/opamTypes.mli index 0a541d2249c..2bf2e7b366f 100644 --- a/src/format/opamTypes.mli +++ b/src/format/opamTypes.mli @@ -299,7 +299,7 @@ type universe = { type pin_kind = [ `version | OpamUrl.backend ] (** Shell compatibility modes *) -type shell = [`fish|`csh|`zsh|`sh|`bash] +type shell = [`fish|`csh|`zsh|`sh|`bash|`cmd] (** {2 Generic command-line definitions with filters} *) diff --git a/src/format/opamTypesBase.ml b/src/format/opamTypesBase.ml index a0789c08e42..1a0c821d266 100644 --- a/src/format/opamTypesBase.ml +++ b/src/format/opamTypesBase.ml @@ -47,6 +47,7 @@ let string_of_shell = function | `zsh -> "zsh" | `sh -> "sh" | `bash -> "bash" + | `cmd -> "Windows Command Processor" let file_null = "" let pos_file filename = OpamFilename.to_string filename, -1, -1 diff --git a/src/state/opamEnv.ml b/src/state/opamEnv.ml index 9adaca86cb6..be7f2576039 100644 --- a/src/state/opamEnv.ml +++ b/src/state/opamEnv.ml @@ -306,6 +306,15 @@ let is_up_to_date st = let opamswitch = OpamStateConfig.(!r.switch_from <> `Default) in is_up_to_date_raw (updates ~opamswitch ~force_path:false st) +let shell_eval_string ?(root="") ?(switch="") shell = + match shell with + | `fish -> + Printf.sprintf "eval (opam env%s%s)" root switch + | `cmd -> + Printf.sprintf "opam env%s%s" root switch + | _ -> + Printf.sprintf "eval $(opam env%s%s)" root switch + let eval_string gt switch = let root = let opamroot_cur = OpamFilename.Dir.to_string gt.root in @@ -332,11 +341,7 @@ let eval_string gt switch = if Some sw_cur <> sw_env then Printf.sprintf " --switch=%s" sw_cur else "" in - match OpamStd.Sys.guess_shell_compat () with - | `fish -> - Printf.sprintf "eval (opam env%s%s)" root switch - | _ -> - Printf.sprintf "eval $(opam env%s%s)" root switch + shell_eval_string (OpamStd.Sys.guess_shell_compat ()) ~root ~switch @@ -347,16 +352,19 @@ let complete_zsh = "complete.zsh" let variables_sh = "variables.sh" let variables_csh = "variables.csh" let variables_fish = "variables.fish" +let variables_cmd = "variables.cmd" let init_sh = "init.sh" let init_zsh = "init.zsh" let init_csh = "init.csh" let init_fish = "init.fish" +let init_cmd = "init.cmd" let init_file = function | `sh -> init_sh | `csh -> init_csh | `zsh -> init_zsh | `bash -> init_sh | `fish -> init_fish + | `cmd -> init_cmd let source root ~shell ?(interactive_only=false) f = let file f = OpamFilename.to_string (OpamPath.init root // f) in @@ -385,13 +393,13 @@ let string_of_update st shell updates = let make_comment comment_opt = OpamStd.Option.to_string (Printf.sprintf "# %s\n") comment_opt in - let sh (k,v,comment) = + let sh _ (k,v,comment) = Printf.sprintf "%s%s=%s; export %s;\n" (make_comment comment) k v k in - let csh (k,v,comment) = + let csh _ (k,v,comment) = Printf.sprintf "%sif ( ! ${?%s} ) setenv %s \"\"\nsetenv %s %s\n" (make_comment comment) k k k v in - let fish (k,v,comment) = + let fish _ (k,v,comment) = (* Fish converts some colon-separated vars to arrays, which have to be treated differently. MANPATH is handled automatically, so better not to set it at all when not already defined*) @@ -415,30 +423,64 @@ let string_of_update st shell updates = Printf.sprintf "%sset -gx %s %s;\n" (make_comment comment) k v in + let cmd p (k,v,_) = Printf.sprintf "%sset %s=%s\n" p k v in let export = match shell with | `zsh | `sh -> sh | `fish -> fish - | `csh -> csh in + | `csh -> csh + | `cmd -> cmd in let aux (ident, symbol, string, comment) = let string = OpamFilter.expand_string ~default:(fun _ -> "") fenv string |> OpamStd.Env.escape_single_quotes ~using_backslashes:(shell = `fish) in + let prefix, string = + if OpamStd.Sys.(os () = Win32) && ident = "MANPATH" then + (Printf.sprintf "for /f \"delims=\" %%%%D in ('cygpath \"%s\"') do " string, "%%D") + else + ("", string) in let key, value = + let separator = match ident with + | "PATH" | "CAML_LD_LIBRARY_PATH" | "PERL5LIB" -> + OpamStd.Sys.path_sep () + | _ -> + ':' in + let retrieve = + if OpamStd.Sys.(os () = Win32) then + fun () -> Printf.sprintf "%%%s%%" + else + fun () -> Printf.sprintf "\"$%s\"" + in + let squote = + if OpamStd.Sys.(os () = Win32) then + fun () x -> x + else + fun () -> Printf.sprintf "'%s'" + in ident, match symbol with - | Eq -> Printf.sprintf "'%s'" string + | Eq -> Printf.sprintf "%a" squote string | PlusEq | ColonEq | EqPlusEq -> - Printf.sprintf "'%s':\"$%s\"" string ident + Printf.sprintf "%a%c%a" squote string separator retrieve ident | EqColon | EqPlus -> - Printf.sprintf "\"$%s\":'%s'" ident string + Printf.sprintf "%a%c%a" retrieve ident separator squote string in - export (key, value, comment) in + export prefix (key, value, comment) in OpamStd.List.concat_map "" aux updates +let rem = function + `cmd -> + "rem" +| _ -> + "#" + let init_script root ~completion ~shell (variables_sh, complete_sh) = let variables = - Some (source root ~shell variables_sh) in + match shell with + `cmd -> + Some "opam env" + | _ -> + Some (source root ~shell variables_sh) in let complete = if completion then OpamStd.Option.map (source root ~shell ~interactive_only:true) complete_sh @@ -448,7 +490,7 @@ let init_script root ~completion ~shell let append name = function | None -> () | Some c -> - Printf.bprintf buf "# %s\n%s\n" name c in + Printf.bprintf buf "%s %s\n%s\n" (rem shell) name c in append "Load the environment variables" variables; append "Load the auto-complete scripts" complete; Buffer.contents buf @@ -462,30 +504,53 @@ let write_script root (name, body) = let write_static_init_scripts root ~completion = let scripts = + let shells = + (* The shell scripts have been intentionally disabled for Windows, the idea being that they'll + * be selectively re-integrated by someone who actually tests them... + *) + if OpamStd.Sys.(os () = Win32) then + [ + `cmd, init_cmd, (variables_cmd, None); + ] + else + [ + `sh, init_sh, (variables_sh, Some complete_sh); + `zsh, init_zsh, (variables_sh, Some complete_zsh); + `csh, init_csh, (variables_csh, None); + `fish, init_fish, (variables_fish, None); + ] in + let scripts = + if OpamStd.Sys.(os () = Win32) then + [ + ] + else + [ + complete_sh, OpamScript.complete; + complete_zsh, OpamScript.complete_zsh; + ] in List.map (fun (shell, init, scripts) -> - init, init_script root ~shell ~completion scripts) [ - `sh, init_sh, (variables_sh, Some complete_sh); - `zsh, init_zsh, (variables_sh, Some complete_zsh); - `csh, init_csh, (variables_csh, None); - `fish, init_fish, (variables_fish, None); - ] @ [ - complete_sh, OpamScript.complete; - complete_zsh, OpamScript.complete_zsh; - ] + init, init_script root ~shell ~completion scripts) shells @ scripts in List.iter (write_script root) scripts let write_dynamic_init_scripts st = let updates = updates ~opamswitch:false st in + let scripts = + if OpamStd.Sys.(os () = Win32) then + [ + variables_cmd, string_of_update st `cmd updates; + ] + else + [ + variables_sh, string_of_update st `sh updates; + variables_csh, string_of_update st `csh updates; + variables_fish, string_of_update st `fish updates; + ] in try OpamFilename.with_flock_upgrade `Lock_write ~dontblock:true st.switch_global.global_lock @@ fun _ -> - List.iter (write_script st.switch_global.root) [ - variables_sh, string_of_update st `sh updates; - variables_csh, string_of_update st `csh updates; - variables_fish, string_of_update st `fish updates; - ] + List.iter (write_script st.switch_global.root) scripts with OpamSystem.Locked -> OpamConsole.warning "Global shell init scripts not installed (could not acquire lock)" @@ -556,7 +621,8 @@ let update_dot_profile root dot_profile shell = let update_user_setup root ?dot_profile shell = if dot_profile <> None then ( OpamConsole.msg "User configuration:\n"; - OpamStd.Option.iter (fun f -> update_dot_profile root f shell) dot_profile + if shell <> `cmd then + OpamStd.Option.iter (fun f -> update_dot_profile root f shell) dot_profile ) let display_setup root ~dot_profile shell = @@ -592,14 +658,20 @@ let display_setup root ~dot_profile shell = OpamConsole.msg "Global configuration:\n"; List.iter print global_setup +let set_cmd_env env = + List.iter (fun (k, v, _) -> log "parent-putenv: %s->%S" k v; ignore (OpamStd.Win32.parent_putenv k v)) env + let check_and_print_env_warning st = if (OpamFile.Config.switch st.switch_global.config = Some st.switch || OpamStateConfig.(!r.switch_from <> `Command_line)) && not (is_up_to_date st) then - OpamConsole.formatted_msg - "# Run %s to update the current shell environment\n" - (OpamConsole.colorise `bold (eval_string st.switch_global - (Some st.switch))) + if OpamStd.Sys.(os () = Win32) then + set_cmd_env (get_opam ~force_path:false st) + else + OpamConsole.formatted_msg + "# Run %s to update the current shell environment\n" + (OpamConsole.colorise `bold (eval_string st.switch_global + (Some st.switch))) let setup_interactive root ~dot_profile shell = let update dot_profile = @@ -611,41 +683,49 @@ let setup_interactive root ~dot_profile shell = OpamConsole.msg "\n"; OpamConsole.header_msg "Required setup - please read"; + if shell <> `cmd then + OpamConsole.msg + "\n\ + \ In normal operation, opam only alters files within ~%s.opam.\n\ + \n\ + \ However, to best integrate with your system, some environment variables\n\ + \ should be set. If you allow it to, this initialisation step will update\n\ + \ your %s configuration by adding the following line to %s:\n\ + \n\ + \ %s\ + \n\ + \ Otherwise, e" + Filename.dir_sep + (OpamConsole.colorise `bold @@ string_of_shell shell) + (OpamConsole.colorise `cyan @@ OpamFilename.prettify dot_profile) + (OpamConsole.colorise `bold @@ source root ~shell (init_file shell)) + else + OpamConsole.msg "E"; OpamConsole.msg - "\n\ - \ In normal operation, opam only alters files within ~%s.opam.\n\ - \n\ - \ However, to best integrate with your system, some environment variables\n\ - \ should be set. If you allow it to, this initialisation step will update\n\ - \ your %s configuration by adding the following line to %s:\n\ - \n\ - \ %s\ - \n\ - \ Otherwise, every time you want to access your opam installation, you will\n\ + "very time you want to access your opam installation, you will\n\ \ need to run:\n\ \n\ \ %s\n\ \n\ \ You can always re-run this setup with 'opam init' later.\n\n" - Filename.dir_sep - (OpamConsole.colorise `bold @@ string_of_shell shell) - (OpamConsole.colorise `cyan @@ OpamFilename.prettify dot_profile) - (OpamConsole.colorise `bold @@ source root ~shell (init_file shell)) - (OpamConsole.colorise `bold @@ "eval $(opam env)"); - match - OpamConsole.read - "Do you want opam to modify %s ? [N/y/f]\n\ - (default is 'no', use 'f' to choose a different file)" - (OpamFilename.prettify dot_profile) - with - | None when OpamCoreConfig.(!r.answer <> None) -> update (Some dot_profile) - | Some ("y" | "Y" | "yes" | "YES" ) -> update (Some dot_profile) - | Some ("f" | "F" | "file" | "FILE") -> - begin match OpamConsole.read " Enter the name of the file to update:" with - | None -> - OpamConsole.msg "Alright, assuming you changed your mind, \ - not performing any changes.\n"; - false - | Some f -> update (Some (OpamFilename.of_string f)) - end - | _ -> update None + (OpamConsole.colorise `bold @@ shell_eval_string shell); + if shell = `cmd then + update None + else + match + OpamConsole.read + "Do you want opam to modify %s ? [N/y/f]\n\ + (default is 'no', use 'f' to choose a different file)" + (OpamFilename.prettify dot_profile) + with + | None when OpamCoreConfig.(!r.answer <> None) -> update (Some dot_profile) + | Some ("y" | "Y" | "yes" | "YES" ) -> update (Some dot_profile) + | Some ("f" | "F" | "file" | "FILE") when shell <> `cmd -> + begin match OpamConsole.read " Enter the name of the file to update:" with + | None -> + OpamConsole.msg "Alright, assuming you changed your mind, \ + not performing any changes.\n"; + false + | Some f -> update (Some (OpamFilename.of_string f)) + end + | _ -> update None diff --git a/src/state/opamEnv.mli b/src/state/opamEnv.mli index 482fc896be7..a301f9f8847 100644 --- a/src/state/opamEnv.mli +++ b/src/state/opamEnv.mli @@ -91,3 +91,6 @@ val clear_dynamic_init_scripts: rw global_state -> unit (** Print a warning if the environment is not set-up properly. (General message) *) val check_and_print_env_warning: 'a switch_state -> unit + +(** Set the environment in the parent process (Windows only) *) +val set_cmd_env : env -> unit