123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372(* Documentation: https://info.nrao.edu/computing/guide/file-access-and-archiving/7zip/7z-7za-command-line-guide
Beware that the 7zip configuration file is usually CRLF, and must always be
UTF-8. *)openBos(** Highest compression. *)letsevenz_compression_level_opts=Cmd.v"-mx9"letsevenz_log_level_opts=(* 7z is super chatty! *)letoutput_log_level_min=Cmd.v"-bb0"inletoutput_log_level_max=Cmd.v"-bb3"inletdisable_stdout_stream=Cmd.v"-bso0"inmatchLogs.level()with|SomeDebug->output_log_level_max|SomeInfo->Cmd.(output_log_level_min%%disable_stdout_stream)|_->disable_stdout_streamletcreate_7z_archive~sevenz_exe~(install_direction:Dkml_install_register.install_direction)~abi_selector~work_dir~archive_path~archive_dir=let(let*)=Rresult.R.bindinletpwd=Dkml_package_console_common.get_ok_or_failwith_rresult(OS.Dir.current())inletarchive_rel_dir=ifFpath.is_relarchive_dirthenFpath.(v"."//archive_dir)elsematchFpath.relativize~root:pwdarchive_dirwith|Somev->v|None->letmsg=Fmt.str"The archive directory %a cannot be made relative to %a"Fpath.pparchive_dirFpath.pppwdinLogs.err(funl->l"FATAL: %s"msg);failwithmsginletwork_abs_dir=ifFpath.is_relwork_dirthenFpath.(pwd//work_dir)elsework_dirinletrun_7zcmdaction=let*status=OS.Cmd.run_statuscmdinmatchstatuswith|`Exited0->Ok()|`Exitedstatus->letmsg=Fmt.str"%a could not %s. Exited with error code %d"Fpath.ppsevenz_exeactionstatusinLogs.err(funl->l"FATAL: %s"msg);failwithmsg|`Signaledsignal->(* https://stackoverflow.com/questions/1101957/are-there-any-standard-exit-status-codes-in-linux/1535733#1535733 *)letmsg=Fmt.str"%a could not %s. Exited with signal %d"Fpath.ppsevenz_exeactionsignalinLogs.err(funl->l"FATAL: %s"msg);failwithmsgin(* Step 0: Erase existing archive, if any, so that subsequent '7z a'
does not "add" duplicates. *)let*()=OS.File.delete~must_exist:falsearchive_pathin(* Step 1: Bundle up everything in the archive directory *)letcmd_create=Cmd.(v(Fpath.to_stringsevenz_exe)%"a"%%sevenz_log_level_opts%%sevenz_compression_level_opts%"-y"%Fpath.to_stringarchive_path(* DIR/* is 7z's syntax for the contents of DIR *)%Fpath.(to_string(archive_rel_dir/"*")))inLogs.debug(funl->l"Creating 7z archive with: %a"Cmd.ppcmd_create);let*()=run_7zcmd_create"create a self-extracting archive"in(* Step 2
7xS2con.sfx and 7xS2.sfx will autolaunch "setup.exe" (or the first .exe,
which is ambiguous). We'll rename bin/dkml-package-entry.exe so that
it is setup.exe.
Syntax:
rn <archive_name> <src_file_1> <dest_file_1> [ <src_file_2> <dest_file_2> ... ]
Confer: https://documentation.help/7-Zip-18.0/rename.htm
*)letcmd_rename=Cmd.(v(Fpath.to_stringsevenz_exe)%"rn"%%sevenz_log_level_opts%%sevenz_compression_level_opts%"-y"%Fpath.to_stringarchive_path%"bin/dkml-package-entry.exe"%matchinstall_directionwith|Install->"setup.exe"|Uninstall->"uninstall.exe")inLogs.debug(funl->l"Renaming within a 7z archive with: %a"Cmd.ppcmd_rename);let*()=run_7zcmd_rename"rename within a self-extracting archive"in(* Step 3
Need vcruntime140.dll (or later) when 7z autolaunches setup.exe since
the renamed dkml-package-entry.exe was compiled with Visual Studio.
In addition, vc_redist.x64.exe or similar needs to be available if we
can't guarantee the "Visual C++ Redistributable Packages" are already
installed. For example, the OCaml installer does install Visual Studio,
which will install the redistributable packages automatically as part
of the Visual Studio Installer ... but that is the exception and not the
rule. So we always bundle the redistributable packages.
For simplicity we name it `vc_redist.dkml-target-abi.exe`.
https://docs.microsoft.com/en-us/cpp/windows/redistributing-visual-cpp-files
TODO: We also need ucrtbase.dll and api-ms-win-*.dll;
https://docs.microsoft.com/en-us/cpp/porting/upgrade-your-code-to-the-universal-crt?view=msvc-170#deployment-and-redistribution-of-the-universal-crt
*)let*redist_dir_str=OS.Env.req_var"VCToolsRedistDir"inlet*redist_dir=Fpath.of_stringredist_dir_strinlet*redist_dir=OS.Dir.must_existredist_dirinlet*()=letlatest_vccrtarch=(* Get lexographically highest path
ex. x64/Microsoft.VC143.CRT > x64/Microsoft.VC142.CRT *)letbasename_pat="Microsoft.VC$(vcver).CRT"inletcrt_pat=Fpath.(redist_dir/arch/basename_pat)inlet*crt_candidates=OS.Path.querycrt_patinletbest_crt_candidate=List.fold_right(fun(fp_a,defs_a)->function|None->Some(fp_a,defs_a)|Some(fp_b,defs_b)->ifFpath.comparefp_afp_b>0thenSome(fp_a,defs_a)elseSome(fp_b,defs_b))crt_candidatesNoneinmatchbest_crt_candidatewith|None->Rresult.R.error_msgf"No files matched the pattern %a"Fpath.ppcrt_pat|Some(src,defs)->letvcver=Pat.formatdefs(Pat.v"$(vcver)")inOk(src,vcver)inletupdate_with_latest_vcruntimesarch=let*z,vcver=latest_vccrtarchin(* We want to support
https://learn.microsoft.com/en-us/cpp/windows/redistributing-visual-cpp-files?view=msvc-170#install-the-redistributable-packages
eventually. So dump the version of vcruntime which should match the version of vcredist. *)letvcver_file=Fpath.(work_dir/"vcver.txt")inlet*()=OS.File.writefvcver_file"%s"vcverin(* ex. x64/Microsoft.VC142.CRT/vcruntime140.dll, x64/Microsoft.VC142.CRT/vcruntime140_1.dll *)(* 7z u: https://documentation.help/7-Zip-18.0/update.htm *)(* let cmd_add =
Cmd.(
v (Fpath.to_string sevenz_exe)
% "a" %% sevenz_log_level_opts %% sevenz_compression_level_opts % "-y"
% Fpath.to_string archive_path
(* DIR/* is 7z's syntax for the contents of DIR *)
% Fpath.(to_string (archive_dir / "vcver*.txt")))
in
Logs.debug (fun l -> l "Adding to 7z archive with: %a" Cmd.pp cmd_add);
let* () = run_7z cmd_add "add to a self-extracting archive" in *)letcmd_add=Cmd.(v(Fpath.to_stringsevenz_exe)%"a"%%sevenz_log_level_opts%%sevenz_compression_level_opts%"-y"%Fpath.to_stringarchive_path(* DIR/* is 7z's syntax for the contents of DIR *)%Fpath.(to_string(z/"vcruntime*.dll"))%Fpath.(to_string(work_abs_dir/"vcver*.txt")))inLogs.debug(funl->l"Adding to 7z archive with: %a"Cmd.ppcmd_add);run_7zcmd_add"add to a self-extracting archive"inletadd_vcredist~src=(* 7z a: https://documentation.help/7-Zip-18.0/add1.htm *)letcmd_add=Cmd.(v(Fpath.to_stringsevenz_exe)%"a"%%sevenz_log_level_opts%%sevenz_compression_level_opts%"-y"%Fpath.to_stringarchive_path(* DIR/* is 7z's syntax for the contents of DIR *)%(Fpath.to_stringsrc^"*"))inLogs.debug(funl->l"Adding to 7z archive with: %a"Cmd.ppcmd_add);let*()=run_7zcmd_add"add to a self-extracting archive"in(* 7z rn: https://documentation.help/7-Zip-18.0/rename.htm *)letcmd_rename=Cmd.(v(Fpath.to_stringsevenz_exe)%"rn"%%sevenz_log_level_opts%%sevenz_compression_level_opts%"-y"%Fpath.to_stringarchive_path%Fpath.basenamesrc%"vc_redist.dkml-target-abi.exe")inLogs.debug(funl->l"Renaming within a 7z archive with: %a"Cmd.ppcmd_rename);run_7zcmd_rename"rename within a self-extracting archive"inmatchabi_selectorwith|Dkml_install_runner.Path_location.Generic->Ok()|AbiWindows_x86_64->let*()=update_with_latest_vcruntimes"x64"inadd_vcredist~src:Fpath.(redist_dir/"vc_redist.x64.exe")|AbiWindows_x86->let*()=update_with_latest_vcruntimes"x86"inadd_vcredist~src:Fpath.(redist_dir/"vc_redist.x86.exe")|AbiWindows_arm64->let*()=update_with_latest_vcruntimes"arm64"inadd_vcredist~src:Fpath.(redist_dir/"vc_redist.arm64.exe")|Abi_->Ok()inOk()letcreate_sfx_exe~sfx_path~archive_path~installer_path=letwrite_file_contents~outputfile=letrechelperinput=matchinput()with|Some(b,pos,len)->output(Some(b,pos,len));helperinput|None->()inDkml_package_console_common.get_ok_or_failwith_rresult(OS.File.with_inputfile(funinput()->helperinput)())inDkml_package_console_common.get_ok_or_failwith_rresult@@OS.File.with_outputinstaller_path(funoutput()->(* Mimic DOS command given in 7z documentation:
copy /b 7zS.sfx + config.txt + archive.7z archive.exe *)(* 7zS.sfx or something similar and perhaps its manifest customized *)write_file_contents~outputsfx_path;(* archive.7z *)write_file_contents~outputarchive_path;(* EOF *)outputNone;Okinstaller_path)()letmodify_manifest~pe_file~work_dir~organization~program_name~program_version=let(let*)=Rresult.R.bindinlettranslates=Str.(s|>global_replace(regexp_string"__PLACEHOLDER_ORG_NOSPACE__")organization.Dkml_package_console_common.Author_types.common_name_camel_case_nospaces|>global_replace(regexp_string"__PLACEHOLDER_PROGRAM_NOSPACE__")program_name.Dkml_package_console_common.Author_types.name_camel_case_nospaces|>global_replace(regexp_string"__PLACEHOLDER_VERSION_MNOP__")(Dkml_package_console_common.version_m_n_o_pprogram_version))inlet*manifest=letpath=Fpath.(work_dir/"setup.exe.manifest")inletcontent=Option.get(Manifests.read"setup.exe.manifest")inlet*()=OS.File.writepath(translatecontent)inOkpathinlet*mt_exe=OS.Cmd.get_tool(Cmd.v"mt")inletcmd=Cmd.(v(Fpath.to_stringmt_exe)%"-manifest"%Fpath.to_stringmanifest%"-verbose"%"-validate_manifest"%"-nologo"%Fmt.str"-outputresource:%a;1"Fpath.pppe_file)inlet*status=OS.Cmd.run_statuscmdinmatchstatuswith|`Exited0->Ok()|`Exitedstatus->letmsg=Fmt.str"%a could not modify the manifest. Exited with error code %d"Fpath.ppmt_exestatusinLogs.err(funl->l"FATAL: %s"msg);failwithmsg|`Signaledsignal->(* https://stackoverflow.com/questions/1101957/are-there-any-standard-exit-status-codes-in-linux/1535733#1535733 *)letmsg=Fmt.str"%a could not modify the manifest. Exited with signal %d"Fpath.ppmt_exesignalinLogs.err(funl->l"FATAL: %s"msg);failwithmsgletgenerate~(install_direction:Dkml_install_register.install_direction)~archive_dir~target_dir~abi_selector~organization~program_name~program_version~work_dir=letabi_name=Dkml_install_runner.Path_location.show_abi_selectorabi_selectorinletprogram_name_kebab_lower_case=program_name.Dkml_package_console_common.Author_types.name_kebab_lower_caseinletdirection=matchinstall_directionwithInstall->"i"|Uninstall->"u"inletinstaller_basename=Fmt.str"unsigned-%s-%s-%s-%s.exe"program_name_kebab_lower_caseabi_namedirectionprogram_versioninletinstaller_path=Fpath.(target_dir/installer_basename)inLogs.info(funl->l"Generating %a"Fpath.ppinstaller_path);Dkml_package_console_common.get_ok_or_failwith_rresult(let(let*)=Rresult.R.bindinletsfx_dir=Fpath.(work_dir/"sfx")inletarchive_path=Fpath.(target_dir/Fmt.str"%s-%s-%s-%s.7z"program_name_kebab_lower_caseabi_namedirectionprogram_version)inletsevenz_exe=Fpath.(sfx_dir/"7zr.exe")inlet*(_was_created:bool)=OS.Dir.createsfx_dirinlet*()=OS.File.write~mode:0o750sevenz_exe(Option.get(Seven_z.read"7zr.exe"))inletsfx=Option.get(Seven_z.read"7zS2con.sfx")in(* Step 1. Create custom 7zS2con.sfx.
If we did MtExeModifiedManifest(SFX || ARCHIVE) then mt.exe would
corrupt the 7zip archive (it would insert RT_MANIFEST resources at the
end of the SFX executable, overwriting the 7zip 32-byte signature that
SFX uses to find the start of the 7zip archive. Results in:
7-Zip Error: Can't find 7z archive
But we can do MtExeModifiedManifest(SFX) || ARCHIVE which preserves
the 7zip archive. Even signing after with
SignToolExe(MtExeModifiedManifest(SFX) || ARCHIVE) should be fine
because the Authenticode procedure used in PE (modern .exe) files
by signtool.exe will safely update the executable sections and
correctly hash the "extra data" (the ARCHIVE) after the executable
sections; confer: http://download.microsoft.com/download/9/c/5/9c5b2167-8017-4bae-9fde-d599bac8184a/authenticode_pe.docx
*)letsfx_basename=Fmt.str"%s-%s-%s-%s.sfx"program_name_kebab_lower_caseabi_namedirectionprogram_versioninletsfx_path=Fpath.(target_dir/sfx_basename)inlet*()=OS.File.writesfx_pathsfxinlet*()=modify_manifest~work_dir~pe_file:sfx_path~organization~program_name~program_versionin(* Step 2. Create ARCHIVE *)let*()=create_7z_archive~sevenz_exe~install_direction~abi_selector~work_dir~archive_path~archive_dirin(* Step 3. Create SFX || ARCHIVE. Return sfx.exe *)create_sfx_exe~sfx_path~archive_path~installer_path)