mirror of
https://github.com/enso-org/enso.git
synced 2024-10-04 00:33:36 +03:00
Custom Windows Installer & Uninstaller (#9815)
This PR introduces a new installer and uninstaller for the Windows platform. Both are written in Rust and compiled to a single executable. The executable has no dependencies (other than what is included in the Windows), links the C++ runtime statically if needed. The change is motivated by numerous issues with with the `electron-builder`-generated installers. The new installer should behave better, not have issues with long paths and unblock the `electron-builder` upgrade (which will significantly simplify the workflow definitions). To build an installer, one needs to provide the unpacked application (generated by `electron-builder`) and the `electron-builder` configuration (with a few minor extensions). Code signing is also supported.
This commit is contained in:
parent
ced7ba2de2
commit
a4f56e92aa
@ -4,14 +4,16 @@ rustflags = ["--cfg", "tokio_unstable"]
|
||||
|
||||
[target.wasm32-unknown-unknown]
|
||||
rustflags = [
|
||||
# Increas the stack size from 1MB to 2MB. This is required to avoid running out of stack space
|
||||
# in debug builds. The error is reported as `RuntimeError: memory access out of bounds`.
|
||||
"-C",
|
||||
"link-args=-z stack-size=2097152",
|
||||
# Increas the stack size from 1MB to 2MB. This is required to avoid running out of stack space
|
||||
# in debug builds. The error is reported as `RuntimeError: memory access out of bounds`.
|
||||
"-C",
|
||||
"link-args=-z stack-size=2097152",
|
||||
]
|
||||
|
||||
[target.x86_64-pc-windows-msvc]
|
||||
rustflags = ["-C", "link-arg=/STACK:2097152"]
|
||||
# Static linking is required to avoid the need for the Visual C++ Redistributable. We care about this primarily for our
|
||||
# installer binary package.
|
||||
rustflags = ["-C", "link-arg=/STACK:2097152", "-C", "target-feature=+crt-static"]
|
||||
|
||||
[target.x86_64-pc-windows-gnu]
|
||||
rustflags = ["-C", "link-arg=-Wl,--stack,2097152"]
|
||||
|
4
.gitignore
vendored
4
.gitignore
vendored
@ -117,6 +117,10 @@ project/metals.sbt
|
||||
/build.json
|
||||
/app/ide-desktop/lib/client/electron-builder-config.json
|
||||
|
||||
# Resources fire generated build-time for Win installer/uninstaller.
|
||||
/build/install/installer/archive.rc
|
||||
/build/install/icon.rc
|
||||
|
||||
|
||||
#################
|
||||
## Build Cache ##
|
||||
|
645
Cargo.lock
generated
645
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
25
Cargo.toml
25
Cargo.toml
@ -12,6 +12,10 @@ members = [
|
||||
"build/macros/proc-macro",
|
||||
"build/ci-gen",
|
||||
"build/cli",
|
||||
"build/install",
|
||||
"build/install/config",
|
||||
"build/install/installer",
|
||||
"build/install/uninstaller",
|
||||
"lib/rust/*",
|
||||
"lib/rust/parser/doc-parser",
|
||||
"lib/rust/parser/src/syntax/tree/visitor",
|
||||
@ -89,23 +93,38 @@ blocks_in_conditions = "allow" # Until the issue is fixed: https://github.com/ru
|
||||
# !!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||
# We are tryingto maintain minimum set of dependencies. Before adding a new dependency, consult it
|
||||
# with the core development team. Thank you!
|
||||
chrono = { version = "0.4.31", features = ["serde"] }
|
||||
clap = { version = "4.5.4", features = ["derive", "env", "wrap_help", "string"] }
|
||||
derive-where = "1.2.7"
|
||||
directories = { version = "5.0.1" }
|
||||
dirs = { version = "5.0.1" }
|
||||
flate2 = { version = "1.0.28" }
|
||||
indicatif = { version = "0.17.7", features = ["tokio"] }
|
||||
multimap = { version = "0.9.1" }
|
||||
native-windows-gui = { version = "1.0.13" }
|
||||
nix = { version = "0.27.1" }
|
||||
octocrab = { git = "https://github.com/enso-org/octocrab", default-features = false, features = [
|
||||
"rustls",
|
||||
] }
|
||||
path-absolutize = "3.1.1"
|
||||
platforms = { version = "3.2.0", features = ["serde"] }
|
||||
portpicker = { version = "0.1.1" }
|
||||
regex = { version = "1.6.0" }
|
||||
serde = { version = "1.0.130", features = ["derive", "rc"] }
|
||||
serde_yaml = { version = "0.9.16" }
|
||||
sha2 = { version = "0.10.8" }
|
||||
sysinfo = { version = "0.30.7" }
|
||||
tokio = { version = "1.23.0", features = ["full", "tracing"] }
|
||||
tokio-stream = { version = "0.1.12", features = ["fs"] }
|
||||
tokio-util = { version = "0.7.4", features = ["full"] }
|
||||
tar = { version = "0.4.40" }
|
||||
tokio = { version = "1.37.0", features = ["full", "tracing"] }
|
||||
tokio-stream = { version = "0.1.15", features = ["fs"] }
|
||||
tokio-util = { version = "0.7.10", features = ["full"] }
|
||||
tracing = { version = "0.1.40" }
|
||||
tracing-subscriber = { version = "0.3.18", features = ["env-filter"] }
|
||||
walkdir = { version = "2.5.0" }
|
||||
wasm-bindgen = { version = "0.2.92", features = [] }
|
||||
wasm-bindgen-test = { version = "0.3.34" }
|
||||
windows = { version = "0.52.0", features = ["Win32", "Win32_UI", "Win32_UI_Shell", "Win32_System", "Win32_System_LibraryLoader", "Win32_Foundation", "Win32_System_Com"] }
|
||||
winreg = { version = "0.52.0" }
|
||||
anyhow = { version = "1.0.66" }
|
||||
failure = { version = "0.1.8" }
|
||||
derive_more = { version = "0.99" }
|
||||
|
@ -40,6 +40,23 @@ export interface Arguments {
|
||||
readonly platform: electronBuilder.Platform
|
||||
}
|
||||
|
||||
/** File association configuration, extended with information needed by the `enso-installer`. */
|
||||
interface ExtendedFileAssociation extends electronBuilder.FileAssociation {
|
||||
/** The Windows registry key under which the file association is registered.
|
||||
*
|
||||
* Should follow the pattern `Enso.CamelCaseName`. */
|
||||
readonly progId: string
|
||||
}
|
||||
|
||||
/** Additional configuration for the installer. */
|
||||
interface InstallerAdditionalConfig {
|
||||
/** The company name to be used in the installer. */
|
||||
readonly publisher: string
|
||||
|
||||
/** File association configuration. */
|
||||
readonly fileAssociations: ExtendedFileAssociation[]
|
||||
}
|
||||
|
||||
//======================================
|
||||
// === Argument parser configuration ===
|
||||
//======================================
|
||||
@ -90,13 +107,49 @@ export const args: Arguments = await yargs(process.argv.slice(2))
|
||||
// === Electron builder configuration ===
|
||||
// ======================================
|
||||
|
||||
/** File associations for the IDE. */
|
||||
export const EXTENDED_FILE_ASSOCIATIONS = [
|
||||
{
|
||||
ext: fileAssociations.SOURCE_FILE_EXTENSION,
|
||||
name: `${common.PRODUCT_NAME} Source File`,
|
||||
role: 'Editor',
|
||||
// Note that MIME type is used on Windows by the enso-installer to register the file association.
|
||||
// This behavior is unlike what electron-builder does.
|
||||
mimeType: 'text/plain',
|
||||
progId: 'Enso.Source',
|
||||
},
|
||||
{
|
||||
ext: fileAssociations.BUNDLED_PROJECT_EXTENSION,
|
||||
name: `${common.PRODUCT_NAME} Project Bundle`,
|
||||
role: 'Editor',
|
||||
mimeType: 'application/gzip',
|
||||
progId: 'Enso.ProjectBundle',
|
||||
},
|
||||
]
|
||||
|
||||
/** Returns non-extended file associations, as required by the `electron-builder`.
|
||||
*
|
||||
* Note that we need to actually remove any additional fields that we added to the file associations,
|
||||
* as the `electron-builder` will error out if it encounters unknown fields. */
|
||||
function getFileAssociations(): electronBuilder.FileAssociation[] {
|
||||
return EXTENDED_FILE_ASSOCIATIONS.map(assoc => {
|
||||
const { ext, name, role, mimeType } = assoc
|
||||
return { ext, name, role, mimeType }
|
||||
})
|
||||
}
|
||||
|
||||
/** Returns additional configuration for the `enso-installer`. */
|
||||
function getInstallerAdditionalConfig(): InstallerAdditionalConfig {
|
||||
return {
|
||||
publisher: common.COMPANY_NAME,
|
||||
fileAssociations: EXTENDED_FILE_ASSOCIATIONS,
|
||||
}
|
||||
}
|
||||
|
||||
/** Based on the given arguments, creates a configuration for the Electron Builder. */
|
||||
export function createElectronBuilderConfig(passedArgs: Arguments): electronBuilder.Configuration {
|
||||
let version = BUILD_INFO.version
|
||||
if (
|
||||
passedArgs.target === 'msi' ||
|
||||
(passedArgs.target == null && process.platform === 'win32')
|
||||
) {
|
||||
if (passedArgs.target === 'msi') {
|
||||
// MSI installer imposes some restrictions on the version number. Namely, product version must have a major
|
||||
// version less than 256, a minor version less than 256, and a build version less than 65536.
|
||||
//
|
||||
@ -110,6 +163,8 @@ export function createElectronBuilderConfig(passedArgs: Arguments): electronBuil
|
||||
productName: common.PRODUCT_NAME,
|
||||
extraMetadata: {
|
||||
version,
|
||||
// This provides extra data for the installer.
|
||||
installer: getInstallerAdditionalConfig(),
|
||||
},
|
||||
copyright: `Copyright © ${new Date().getFullYear()} ${common.COMPANY_NAME}`,
|
||||
|
||||
@ -165,7 +220,7 @@ export function createElectronBuilderConfig(passedArgs: Arguments): electronBuil
|
||||
win: {
|
||||
// Compression is not used as the build time is huge and file size saving
|
||||
// almost zero.
|
||||
target: passedArgs.target ?? 'nsis',
|
||||
target: passedArgs.target ?? 'dir',
|
||||
icon: `${passedArgs.iconsDist}/icon.ico`,
|
||||
},
|
||||
linux: {
|
||||
@ -187,18 +242,7 @@ export function createElectronBuilderConfig(passedArgs: Arguments): electronBuil
|
||||
filter: ['!**.tar.gz', '!**.zip'],
|
||||
},
|
||||
],
|
||||
fileAssociations: [
|
||||
{
|
||||
ext: fileAssociations.SOURCE_FILE_EXTENSION,
|
||||
name: `${common.PRODUCT_NAME} Source File`,
|
||||
role: 'Editor',
|
||||
},
|
||||
{
|
||||
ext: fileAssociations.BUNDLED_PROJECT_EXTENSION,
|
||||
name: `${common.PRODUCT_NAME} Project Bundle`,
|
||||
role: 'Editor',
|
||||
},
|
||||
],
|
||||
fileAssociations: getFileAssociations(),
|
||||
directories: {
|
||||
output: `${passedArgs.ideDist}`,
|
||||
},
|
||||
@ -233,15 +277,15 @@ export function createElectronBuilderConfig(passedArgs: Arguments): electronBuil
|
||||
sign: false,
|
||||
},
|
||||
afterAllArtifactBuild: computeHashes,
|
||||
afterPack: ctx => {
|
||||
afterPack: (context: electronBuilder.AfterPackContext) => {
|
||||
if (passedArgs.platform === electronBuilder.Platform.MAC) {
|
||||
// Make the subtree writable, so we can sign the binaries.
|
||||
// This is needed because GraalVM distribution comes with read-only binaries.
|
||||
childProcess.execFileSync('chmod', ['-R', 'u+w', ctx.appOutDir])
|
||||
childProcess.execFileSync('chmod', ['-R', 'u+w', context.appOutDir])
|
||||
}
|
||||
},
|
||||
|
||||
afterSign: async context => {
|
||||
afterSign: async (context: electronBuilder.AfterPackContext) => {
|
||||
// Notarization for macOS.
|
||||
if (
|
||||
passedArgs.platform === electronBuilder.Platform.MAC &&
|
||||
@ -286,6 +330,17 @@ export function createElectronBuilderConfig(passedArgs: Arguments): electronBuil
|
||||
}
|
||||
}
|
||||
|
||||
/** Write the configuration to a JSON file.
|
||||
*
|
||||
* On Windows it is necessary to provide configuration to our installer. On other platforms, this may be useful for debugging.
|
||||
*
|
||||
* The configuration will be extended with additional information needed by the `enso-installer`.
|
||||
*/
|
||||
async function dumpConfiguration(configPath: string, config: electronBuilder.Configuration) {
|
||||
const jsonConfig = JSON.stringify(config)
|
||||
await fs.writeFile(configPath, jsonConfig)
|
||||
}
|
||||
|
||||
/** Build the IDE package with Electron Builder. */
|
||||
export async function buildPackage(passedArgs: Arguments) {
|
||||
// `electron-builder` checks for presence of `node_modules` directory. If it is not present, it
|
||||
@ -297,10 +352,22 @@ export async function buildPackage(passedArgs: Arguments) {
|
||||
// failing because of that.
|
||||
await fs.mkdir('node_modules', { recursive: true })
|
||||
|
||||
const config = createElectronBuilderConfig(passedArgs)
|
||||
const cliOpts: electronBuilder.CliOptions = {
|
||||
config: createElectronBuilderConfig(passedArgs),
|
||||
config,
|
||||
targets: passedArgs.platform.createTarget(),
|
||||
}
|
||||
|
||||
// If `ENSO_BUILD_ELECTRON_BUILDER_CONFIG` is set, we will write the configuration to the
|
||||
// specified path. Otherwise, we will write it to the default path.
|
||||
// This is used on Windows to provide the configuration to the installer build. On other
|
||||
// platforms, this may be useful for debugging.
|
||||
const configPath =
|
||||
process.env['ENSO_BUILD_ELECTRON_BUILDER_CONFIG'] ??
|
||||
`${passedArgs.ideDist}/electron-builder-config.yaml`
|
||||
console.log(`Writing configuration to ${configPath}`)
|
||||
await dumpConfiguration(configPath, config)
|
||||
|
||||
console.log('Building with configuration:', cliOpts)
|
||||
const result = await electronBuilder.build(cliOpts)
|
||||
console.log('Electron Builder is done. Result:', result)
|
||||
|
@ -35,6 +35,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@electron/notarize": "2.1.0",
|
||||
"@types/node": "^20.10.5",
|
||||
"electron": "25.7.0",
|
||||
"electron-builder": "^22.14.13",
|
||||
"enso-common": "^1.0.0",
|
||||
|
5
app/ide-desktop/lib/types/globals.d.ts
vendored
5
app/ide-desktop/lib/types/globals.d.ts
vendored
@ -6,7 +6,7 @@
|
||||
// This file is being imported for its types.
|
||||
// prettier-ignore
|
||||
// eslint-disable-next-line no-restricted-syntax, @typescript-eslint/consistent-type-imports
|
||||
import * as buildJson from './../../build.json' assert { type: 'json' }
|
||||
import * as buildJson from './../../build.json' assert {type: 'json'}
|
||||
|
||||
// =============
|
||||
// === Types ===
|
||||
@ -94,6 +94,7 @@ declare global {
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
interface ProcessEnv {
|
||||
readonly [key: string]: never
|
||||
|
||||
// These are environment variables, and MUST be in CONSTANT_CASE.
|
||||
/* eslint-disable @typescript-eslint/naming-convention */
|
||||
// This is declared in `@types/node`. It MUST be re-declared here to suppress the error
|
||||
@ -113,6 +114,8 @@ declare global {
|
||||
// @ts-expect-error The index signature is intentional to disallow unknown env vars.
|
||||
readonly ENSO_BUILD_ICONS?: string
|
||||
// @ts-expect-error The index signature is intentional to disallow unknown env vars.
|
||||
readonly ENSO_BUILD_ELECTRON_BUILDER_CONFIG?: string
|
||||
// @ts-expect-error The index signature is intentional to disallow unknown env vars.
|
||||
readonly npm_package_name?: string
|
||||
|
||||
// === Cloud environment variables ===
|
||||
|
@ -12,7 +12,7 @@ futures = { workspace = true }
|
||||
serde = "1.0.145"
|
||||
serde_json = { workspace = true }
|
||||
serde_yaml = { workspace = true }
|
||||
tracing = "0.1.36"
|
||||
tracing = { workspace = true }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
@ -215,6 +215,26 @@ pub fn copy_file_if_different(source: impl AsRef<Path>, target: impl AsRef<Path>
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Writes the given contents to the specified path only if the new contents differ from the
|
||||
/// existing ones.
|
||||
///
|
||||
/// If the file does not exist, it will be created. If the file exists and its contents are
|
||||
/// identical to the new contents, the function will not perform a write operation. This is useful
|
||||
/// for avoiding unnecessary file changes.
|
||||
pub fn write_if_different(path: impl AsRef<Path>, contents: impl AsRef<[u8]>) -> Result {
|
||||
if let Ok(existing_metadata) = metadata(&path) {
|
||||
if existing_metadata.len() == contents.as_ref().len() as u64 {
|
||||
// We allow ? below. If the file exists, it should be readable - or something is wrong.
|
||||
let existing_contents = read(&path)?;
|
||||
if existing_contents == contents.as_ref() {
|
||||
trace!("Contents are identical, not writing to {}.", path.as_ref().display());
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
}
|
||||
write(&path, contents)
|
||||
}
|
||||
|
||||
/// Append contents to the file.
|
||||
///
|
||||
/// If the file does not exist, it will be created.
|
||||
|
@ -12,7 +12,7 @@ aws-sdk-s3 = "0.21.0"
|
||||
base64 = "0.13.0"
|
||||
bytes = { workspace = true }
|
||||
byte-unit = { workspace = true }
|
||||
chrono = { version = "0.4.19", features = ["serde"] }
|
||||
chrono = { workspace = true }
|
||||
clap = { workspace = true }
|
||||
derivative = { workspace = true }
|
||||
derive_more = { workspace = true }
|
||||
@ -25,6 +25,7 @@ heck = "0.4.0"
|
||||
enso-build-base = { path = "../base" }
|
||||
enso-enso-font = { path = "../../lib/rust/enso-font" }
|
||||
enso-font = { path = "../../lib/rust/font" }
|
||||
enso-install-config = { path = "../install/config" }
|
||||
ide-ci = { path = "../ci_utils" }
|
||||
mime = "0.3.16"
|
||||
new_mime_guess = "4.0.1"
|
||||
@ -39,15 +40,16 @@ serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
serde_yaml = { workspace = true }
|
||||
scopeguard = "1.1.0"
|
||||
sha2 = { workspace = true }
|
||||
strum = { workspace = true }
|
||||
sysinfo = { workspace = true }
|
||||
tempfile = "3.2.0"
|
||||
toml = "0.5.8"
|
||||
tokio = { workspace = true }
|
||||
tracing = { version = "0.1.37" }
|
||||
tracing = { workspace = true }
|
||||
url = "2.2.2"
|
||||
uuid = { version = "1.1.0", features = ["v4"] }
|
||||
walkdir = "2.3.2"
|
||||
walkdir = { workspace = true }
|
||||
zip = { version = "0.6.2", default-features = false, features = ["deflate"] }
|
||||
|
||||
[build-dependencies]
|
||||
|
@ -3,10 +3,12 @@
|
||||
|
||||
use ide_ci::prelude::*;
|
||||
|
||||
use ide_ci::programs::cargo::build::rerun_if_file_changed;
|
||||
|
||||
|
||||
|
||||
fn main() -> Result {
|
||||
println!("cargo:rerun-if-changed=paths.yaml");
|
||||
rerun_if_file_changed("paths.yaml");
|
||||
let yaml_contents = include_bytes!("paths.yaml");
|
||||
let code = enso_build_macros_lib::paths::process(yaml_contents.as_slice())?;
|
||||
let out_dir = ide_ci::programs::cargo::build_env::OUT_DIR.get()?;
|
||||
|
@ -108,6 +108,7 @@
|
||||
enso-pack/:
|
||||
dist/: # Here ensogl-pack outputs its artifacts
|
||||
generated-java/:
|
||||
rust/:
|
||||
test-results/:
|
||||
test/:
|
||||
Benchmarks/:
|
||||
|
@ -43,7 +43,7 @@ mod tests {
|
||||
#[tokio::test]
|
||||
#[ignore]
|
||||
async fn test_name() -> Result {
|
||||
setup_logging()?;
|
||||
setup_logging().ok();
|
||||
let tag = "test_runtime_image";
|
||||
info!("Current directory: {}", ide_ci::env::current_dir()?.display());
|
||||
let root = deduce_repository_path()?;
|
||||
|
@ -389,6 +389,10 @@ pub fn expose_os_specific_signing_secret(os: OS, step: Step) -> Step {
|
||||
&crate::ide::web::env::APPLETEAMID,
|
||||
)
|
||||
.with_env(crate::ide::web::env::CSC_IDENTITY_AUTO_DISCOVERY, "true")
|
||||
// `CSC_FOR_PULL_REQUEST` can potentially expose sensitive information to third-party,
|
||||
// see the comment in the definition of `CSC_FOR_PULL_REQUEST` for more information.
|
||||
//
|
||||
// In our case, we are safe here, as any PRs from forks do not get the secrets exposed.
|
||||
.with_env(crate::ide::web::env::CSC_FOR_PULL_REQUEST, "true"),
|
||||
_ => step,
|
||||
}
|
||||
|
@ -117,7 +117,7 @@ mod tests {
|
||||
|
||||
#[tokio::test]
|
||||
async fn check_node_version() -> Result {
|
||||
setup_logging()?;
|
||||
setup_logging().ok();
|
||||
|
||||
let version = Node.parse_version("v16.13.2")?;
|
||||
let requirement = VersionReq::parse("=16.15.0")?;
|
||||
@ -128,7 +128,7 @@ mod tests {
|
||||
#[tokio::test]
|
||||
#[ignore]
|
||||
async fn deserialize() -> Result {
|
||||
setup_logging()?;
|
||||
setup_logging().ok();
|
||||
let config = r#"
|
||||
# Options intended to be common for all developers.
|
||||
wasm-size-limit: "4.37MB"
|
||||
@ -147,7 +147,7 @@ required-versions:
|
||||
|
||||
#[tokio::test]
|
||||
async fn deserialize_config_in_repo() -> Result {
|
||||
setup_logging()?;
|
||||
setup_logging().ok();
|
||||
// let config = include_str!("../../../build-config.yaml");
|
||||
let config = r#"# Options intended to be common for all developers.
|
||||
|
||||
|
@ -66,7 +66,7 @@ mod tests {
|
||||
#[tokio::test]
|
||||
#[ignore]
|
||||
async fn spawn() -> Result {
|
||||
setup_logging()?;
|
||||
setup_logging().ok();
|
||||
set_current_dir(r"H:\NBO\enso5")?;
|
||||
let cache = cache::Cache::new_default().await?;
|
||||
cache::goodie::sbt::Sbt.install_if_missing(&cache).await?;
|
||||
|
@ -1,16 +1,19 @@
|
||||
//! Code for dealing with JS/TS components of the GUI1 and the Electron client (IDE).
|
||||
|
||||
use crate::prelude::*;
|
||||
|
||||
use crate::ide::web::env::CSC_KEY_PASSWORD;
|
||||
use crate::paths::generated;
|
||||
use crate::project::gui::BuildInfo;
|
||||
use crate::project::IsArtifact;
|
||||
use crate::version::ENSO_VERSION;
|
||||
|
||||
use anyhow::Context;
|
||||
use futures_util::future::try_join;
|
||||
use ide_ci::env::known::electron_builder::WindowsSigningCredentials;
|
||||
use ide_ci::io::download_all;
|
||||
use ide_ci::program::command::FallibleManipulator;
|
||||
use ide_ci::programs::node::NpmCommand;
|
||||
use ide_ci::programs::Npm;
|
||||
use sha2::Digest;
|
||||
use std::process::Stdio;
|
||||
use tempfile::TempDir;
|
||||
|
||||
@ -56,51 +59,8 @@ pub mod env {
|
||||
}
|
||||
|
||||
// === Electron Builder ===
|
||||
// Variables introduced by the Electron Builder itself.
|
||||
// See: https://www.electron.build/code-signing
|
||||
pub use ide_ci::env::known::electron_builder::*;
|
||||
|
||||
define_env_var! {
|
||||
/// The HTTPS link (or base64-encoded data, or file:// link, or local path) to certificate
|
||||
/// (*.p12 or *.pfx file). Shorthand ~/ is supported (home directory).
|
||||
WIN_CSC_LINK, String;
|
||||
|
||||
/// The password to decrypt the certificate given in WIN_CSC_LINK.
|
||||
WIN_CSC_KEY_PASSWORD, String;
|
||||
|
||||
/// The HTTPS link (or base64-encoded data, or file:// link, or local path) to certificate
|
||||
/// (*.p12 or *.pfx file). Shorthand ~/ is supported (home directory).
|
||||
CSC_LINK, String;
|
||||
|
||||
/// The password to decrypt the certificate given in CSC_LINK.
|
||||
CSC_KEY_PASSWORD, String;
|
||||
|
||||
/// The username of apple developer account.
|
||||
APPLEID, String;
|
||||
|
||||
/// The app-specific password (not Apple ID password). See:
|
||||
/// https://support.apple.com/HT204397
|
||||
APPLEIDPASS, String;
|
||||
|
||||
/// Apple Team ID.
|
||||
APPLETEAMID, String;
|
||||
|
||||
/// `true` or `false`. Defaults to `true` — on a macOS development machine valid and
|
||||
/// appropriate identity from your keychain will be automatically used.
|
||||
CSC_IDENTITY_AUTO_DISCOVERY, bool;
|
||||
|
||||
/// Path to the python2 executable, used by electron-builder on macOS to package DMG.
|
||||
PYTHON_PATH, PathBuf;
|
||||
|
||||
/// Note that enabling CSC_FOR_PULL_REQUEST can pose serious security risks. Refer to the
|
||||
/// [CircleCI documentation](https://circleci.com/docs/1.0/fork-pr-builds/) for more
|
||||
/// information. If the project settings contain SSH keys, sensitive environment variables,
|
||||
/// or AWS credentials, and untrusted forks can submit pull requests to your repository, it
|
||||
/// is not recommended to enable this option.
|
||||
///
|
||||
/// In our case we are careful to not expose any sensitive information to third-party forks,
|
||||
/// so we can safely enable this option.
|
||||
CSC_FOR_PULL_REQUEST, bool;
|
||||
}
|
||||
|
||||
// Cloud environment configuration
|
||||
define_env_var! {
|
||||
@ -147,6 +107,37 @@ pub mod env {
|
||||
}
|
||||
}
|
||||
|
||||
/// Name of the directory with the unpacked Electron package.
|
||||
///
|
||||
/// The directory is created by the `electron-builder` utility in the output directory when run
|
||||
/// with the `dir` target. It is also usually created for other targets, as it is an intermediate
|
||||
/// step in the packaging process.
|
||||
///
|
||||
/// # Panics
|
||||
/// This function panics if the provided OS and architecture combination is not supported.
|
||||
pub fn unpacked_dir(output_path: impl AsRef<Path>, os: OS, arch: Arch) -> PathBuf {
|
||||
let segment_name = match (os, arch) {
|
||||
(OS::Linux, Arch::X86_64) => "linux-unpacked",
|
||||
(OS::MacOS, Arch::AArch64) => "mac-arm64",
|
||||
(OS::MacOS, Arch::X86_64) => "mac",
|
||||
(OS::Windows, Arch::X86_64) => "win-unpacked",
|
||||
_ => todo!("{os}-{arch} combination is not supported"),
|
||||
};
|
||||
output_path.as_ref().join(segment_name)
|
||||
}
|
||||
|
||||
/// Computes the SHA-256 checksum of a file and writes it to a file.
|
||||
///
|
||||
/// This is a Rust equivalent of the `app/ide-desktop/lib/client/tasks/computeHashes.mjs`.
|
||||
pub fn store_sha256_checksum(file: impl AsRef<Path>, checksum_file: impl AsRef<Path>) -> Result {
|
||||
let mut hasher = sha2::Sha256::new();
|
||||
let mut file = ide_ci::fs::open(&file)?;
|
||||
std::io::copy(&mut file, &mut hasher)?;
|
||||
let hash = hasher.finalize();
|
||||
ide_ci::fs::write(&checksum_file, format!("{hash:x}"))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct IconsArtifacts(pub PathBuf);
|
||||
|
||||
@ -198,12 +189,6 @@ impl AsRef<OsStr> for Workspaces {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub enum Command {
|
||||
Build,
|
||||
Watch,
|
||||
}
|
||||
|
||||
pub fn target_os_flag(os: OS) -> Result<&'static str> {
|
||||
match os {
|
||||
OS::Windows => Ok("--win"),
|
||||
@ -307,7 +292,9 @@ impl IdeDesktop {
|
||||
target_os: OS,
|
||||
target: Option<String>,
|
||||
) -> Result {
|
||||
if TARGET_OS == OS::MacOS && CSC_KEY_PASSWORD.is_set() {
|
||||
let output_path = output_path.as_ref();
|
||||
let electron_config = output_path.join("electron-builder.json");
|
||||
if TARGET_OS == OS::MacOS && env::CSC_KEY_PASSWORD.is_set() {
|
||||
// This means that we will be doing code signing on MacOS. This requires JDK environment
|
||||
// to be set up.
|
||||
let graalvm =
|
||||
@ -318,18 +305,19 @@ impl IdeDesktop {
|
||||
|
||||
crate::web::install(&self.repo_root).await?;
|
||||
let pm_bundle = ProjectManagerInfo::new(project_manager)?;
|
||||
let client_build = self
|
||||
.npm()?
|
||||
self.npm()?
|
||||
.set_env(env::ENSO_BUILD_GUI, gui.as_ref())?
|
||||
.set_env(env::ENSO_BUILD_IDE, output_path.as_ref())?
|
||||
.set_env(env::ENSO_BUILD_IDE, output_path)?
|
||||
.try_applying(&pm_bundle)?
|
||||
.workspace(Workspaces::Enso)
|
||||
.run("build")
|
||||
.run_ok();
|
||||
.run_ok()
|
||||
.await?;
|
||||
|
||||
let icons_dist = TempDir::new()?;
|
||||
let icons_dist = icons_dist.into_path();
|
||||
let icons_build = self.build_icons(&icons_dist);
|
||||
let (icons, _content) = try_join(icons_build, client_build).await?;
|
||||
let icons = icons_build.await?;
|
||||
|
||||
let python_path = if TARGET_OS == OS::MacOS && !env::PYTHON_PATH.is_set() {
|
||||
// On macOS electron-builder will fail during DMG creation if there is no python2
|
||||
@ -358,9 +346,10 @@ impl IdeDesktop {
|
||||
.try_applying(&icons)?
|
||||
// .env("DEBUG", "electron-builder")
|
||||
.set_env(env::ENSO_BUILD_GUI, gui.as_ref())?
|
||||
.set_env(env::ENSO_BUILD_IDE, output_path.as_ref())?
|
||||
.set_env(env::ENSO_BUILD_IDE, output_path)?
|
||||
.set_env(env::ENSO_BUILD_PROJECT_MANAGER, project_manager.as_ref())?
|
||||
.set_env_opt(env::PYTHON_PATH, python_path.as_ref())?
|
||||
.set_env(enso_install_config::ENSO_BUILD_ELECTRON_BUILDER_CONFIG, &electron_config)?
|
||||
.workspace(Workspaces::Enso)
|
||||
// .args(["--loglevel", "verbose"])
|
||||
.run("dist")
|
||||
@ -370,6 +359,33 @@ impl IdeDesktop {
|
||||
.run_ok()
|
||||
.await?;
|
||||
|
||||
// On Windows we build our own installer by invoking `enso_install_config::bundler::bundle`.
|
||||
if TARGET_OS == OS::Windows {
|
||||
let code_signing_certificate = WindowsSigningCredentials::new_from_env()
|
||||
.await
|
||||
.inspect_err(|e| {
|
||||
warn!("Failed to create code signing certificate from the environment: {e:?}");
|
||||
})
|
||||
.ok();
|
||||
|
||||
let ide_artifacts = crate::project::ide::Artifact::new(
|
||||
target_os,
|
||||
TARGET_ARCH,
|
||||
&ENSO_VERSION.get()?,
|
||||
output_path,
|
||||
);
|
||||
|
||||
let config = enso_install_config::bundler::Config {
|
||||
electron_builder_config: electron_config,
|
||||
unpacked_electron_bundle: unpacked_dir(output_path, target_os, TARGET_ARCH),
|
||||
repo_root: self.repo_root.to_path_buf(),
|
||||
output_file: ide_artifacts.image.clone(),
|
||||
intermediate_dir: output_path.to_path_buf(),
|
||||
certificate: code_signing_certificate,
|
||||
};
|
||||
enso_install_config::bundler::bundle(config).await?;
|
||||
store_sha256_checksum(&ide_artifacts.image, &ide_artifacts.image_checksum)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
@ -185,7 +185,7 @@ mod tests {
|
||||
#[tokio::test]
|
||||
#[ignore]
|
||||
async fn new_download() -> Result {
|
||||
setup_logging()?;
|
||||
setup_logging().ok();
|
||||
let path = r"C:\temp\google_fonts2";
|
||||
let octocrab = github::setup_octocrab().await?;
|
||||
let cache = Cache::new_default().await?;
|
||||
|
@ -29,13 +29,7 @@ impl Artifact {
|
||||
version: &Version,
|
||||
dist_dir: impl AsRef<Path>,
|
||||
) -> Self {
|
||||
let unpacked = dist_dir.as_ref().join(match target_os {
|
||||
OS::Linux => "linux-unpacked",
|
||||
OS::MacOS if target_arch == Arch::AArch64 => "mac-arm64",
|
||||
OS::MacOS if target_arch == Arch::X86_64 => "mac",
|
||||
OS::Windows => "win-unpacked",
|
||||
_ => todo!("{target_os}-{target_arch} combination is not supported"),
|
||||
});
|
||||
let unpacked = crate::ide::web::unpacked_dir(&dist_dir, target_os, target_arch);
|
||||
let unpacked_executable = match target_os {
|
||||
OS::Linux => "enso",
|
||||
OS::MacOS => "Enso.app",
|
||||
|
@ -328,7 +328,7 @@ mod tests {
|
||||
#[tokio::test]
|
||||
#[ignore]
|
||||
async fn notify_cloud() -> Result {
|
||||
setup_logging()?;
|
||||
setup_logging().ok();
|
||||
let version = Version::from_str("2022.1.1-rc.2")?;
|
||||
notify_cloud_about_gui(&version).await?;
|
||||
Ok(())
|
||||
@ -337,7 +337,7 @@ mod tests {
|
||||
#[tokio::test]
|
||||
#[ignore]
|
||||
async fn release_assets() -> Result {
|
||||
setup_logging()?;
|
||||
setup_logging().ok();
|
||||
|
||||
let crate_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
||||
let repo_root = crate_dir.parent().unwrap().parent().unwrap();
|
||||
|
@ -43,7 +43,7 @@ mod tests {
|
||||
#[tokio::test]
|
||||
#[ignore]
|
||||
async fn manual_call() -> Result {
|
||||
setup_logging()?;
|
||||
setup_logging().ok();
|
||||
let octo = setup_octocrab().await?;
|
||||
build_image_workflow_dispatch_input(&octo, &Version::parse("2022.1.1-nightly.2022-10-18")?)
|
||||
.await?;
|
||||
|
@ -11,7 +11,7 @@ async-trait = "0.1.78"
|
||||
bincode = "1.3.3"
|
||||
byte-unit = { workspace = true }
|
||||
bytes = { workspace = true }
|
||||
chrono = { version = "0.4.19", features = ["serde"] }
|
||||
chrono = { workspace = true }
|
||||
clap = { workspace = true }
|
||||
data-encoding = "2.3.2"
|
||||
dependency_runner = "1.1.0"
|
||||
@ -20,7 +20,7 @@ derive_more = { workspace = true }
|
||||
dirs = { workspace = true }
|
||||
enso-build-base = { path = "../base" }
|
||||
enso-zst = { path = "../../lib/rust/zst" }
|
||||
flate2 = "1.0.22"
|
||||
flate2 = { workspace = true }
|
||||
flume = "0.10.10"
|
||||
fs_extra = "1.3.0"
|
||||
futures = { workspace = true }
|
||||
@ -29,15 +29,14 @@ glob = "0.3.0"
|
||||
headers = "0.3.7"
|
||||
heck = "0.4.0"
|
||||
http-serde = "1.1.0"
|
||||
indicatif = { version = "0.17.1", features = ["tokio"] }
|
||||
indicatif = { workspace = true }
|
||||
itertools = { workspace = true }
|
||||
lazy_static = { workspace = true }
|
||||
log = "0.4.14"
|
||||
mime = "0.3.16"
|
||||
multimap = "0.8.3"
|
||||
multimap = { workspace = true }
|
||||
new_mime_guess = "4.0.0"
|
||||
octocrab = { workspace = true }
|
||||
path-absolutize = "3.0.11"
|
||||
path-absolutize = { workspace = true }
|
||||
pathdiff = "0.2.1"
|
||||
path-slash = "0.2.1"
|
||||
platforms = { workspace = true }
|
||||
@ -53,19 +52,20 @@ sha2 = "0.10.2"
|
||||
strum = { workspace = true }
|
||||
symlink = "0.1.0"
|
||||
sysinfo = { workspace = true }
|
||||
tar = "0.4.37"
|
||||
tar = { workspace = true }
|
||||
tempfile = "3.2.0"
|
||||
tokio = { workspace = true }
|
||||
tokio-stream = { workspace = true }
|
||||
tokio-util = { workspace = true }
|
||||
tracing = { version = "0.1.37" }
|
||||
tracing-subscriber = { version = "0.3.11", features = ["env-filter"] }
|
||||
tracing = { workspace = true }
|
||||
tracing-subscriber = { workspace = true }
|
||||
unicase = "2.6.0"
|
||||
url = "2.2.2"
|
||||
uuid = { version = "1.1.0", features = ["v4", "serde"] }
|
||||
walkdir = "2.3.2"
|
||||
which = "4.2.2"
|
||||
walkdir = { workspace = true }
|
||||
which = "5.0.0"
|
||||
zip = { version = "0.6.2", default-features = false, features = ["deflate"] }
|
||||
base64 = "0.21.0"
|
||||
|
||||
[dev-dependencies]
|
||||
warp = { version = "0.3.2", default-features = false }
|
||||
|
@ -286,7 +286,7 @@ mod tests {
|
||||
#[ignore]
|
||||
async fn test_upload() -> Result {
|
||||
use warp::Filter;
|
||||
setup_logging()?;
|
||||
setup_logging().ok();
|
||||
|
||||
let response1 = CreateArtifactResponse {
|
||||
name: "test-artifact".to_string(),
|
||||
|
@ -7,6 +7,37 @@ use std::fs::File;
|
||||
|
||||
|
||||
|
||||
/// Synchronous version of [`extract_files`].
|
||||
#[context("Failed to extract files from the archive.")]
|
||||
pub fn extract_files_sync<R: Read>(
|
||||
mut archive: tar::Archive<R>,
|
||||
mut filter: impl FnMut(&tar::Entry<R>) -> Option<PathBuf>,
|
||||
) -> Result {
|
||||
let entries = archive.entries()?;
|
||||
for entry in entries {
|
||||
let mut entry = entry?;
|
||||
let path_in_archive = entry.path()?.to_path_buf();
|
||||
if let Some(output_path) = filter(&entry) {
|
||||
let entry_type = entry.header().entry_type();
|
||||
let make_message = |prefix, path: &Path| {
|
||||
format!(
|
||||
"{} {:?} entry: {} => {}",
|
||||
prefix,
|
||||
entry_type,
|
||||
path.display(),
|
||||
output_path.display()
|
||||
)
|
||||
};
|
||||
|
||||
trace!("{}", make_message("Extracting", &path_in_archive));
|
||||
entry
|
||||
.unpack(&output_path)
|
||||
.with_context(|| make_message("Failed to extract", &path_in_archive))?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// ===============
|
||||
// === Archive ===
|
||||
// ===============
|
||||
@ -36,34 +67,13 @@ impl Archive {
|
||||
}
|
||||
|
||||
/// Synchronous version of [`extract_files`].
|
||||
#[context("Failed to extract files from archive {}", self.path.display())]
|
||||
pub fn extract_files_sync(
|
||||
mut self,
|
||||
mut filter: impl FnMut(&Path) -> Option<PathBuf>,
|
||||
self,
|
||||
filter: impl FnMut(&tar::Entry<GzDecoder<File>>) -> Option<PathBuf>,
|
||||
) -> Result {
|
||||
let entries = self.file.entries()?;
|
||||
for entry in entries {
|
||||
let mut entry = entry?;
|
||||
let path_in_archive = entry.path()?;
|
||||
if let Some(output_path) = filter(&path_in_archive) {
|
||||
let entry_type = entry.header().entry_type();
|
||||
let make_message = |prefix, path: Cow<Path>| {
|
||||
format!(
|
||||
"{} {:?} entry: {} => {}",
|
||||
prefix,
|
||||
entry_type,
|
||||
path.display(),
|
||||
output_path.display()
|
||||
)
|
||||
};
|
||||
|
||||
trace!("{}", make_message("Extracting", path_in_archive));
|
||||
entry.unpack(&output_path).with_context(|| {
|
||||
make_message("Failed to extract", entry.path().unwrap_or_default())
|
||||
})?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
extract_files_sync(self.file, filter).with_context(|| {
|
||||
format!("Failed to extract files from archive {}", self.path.display())
|
||||
})
|
||||
}
|
||||
|
||||
/// Extract all files from the specified subtree in the archive, placing them in the specified
|
||||
@ -99,7 +109,8 @@ impl Archive {
|
||||
}
|
||||
|
||||
impl ExtractFiles for Archive {
|
||||
async fn extract_files(self, filter: impl FnMut(&Path) -> Option<PathBuf>) -> Result {
|
||||
async fn extract_files(self, mut filter: impl FnMut(&Path) -> Option<PathBuf>) -> Result {
|
||||
let filter = move |entry: &tar::Entry<GzDecoder<File>>| filter(entry.path().ok()?.as_ref());
|
||||
let job = move || self.extract_files_sync(filter);
|
||||
tokio::task::block_in_place(job)
|
||||
}
|
||||
|
@ -189,7 +189,7 @@ mod tests {
|
||||
#[tokio::test]
|
||||
#[ignore]
|
||||
async fn cache_test() -> Result {
|
||||
setup_logging()?;
|
||||
setup_logging().ok();
|
||||
let download_task = DownloadFile::new("https://store.akamai.steamstatic.com/public/shared/images/header/logo_steam.svg?t=962016")?;
|
||||
|
||||
let cache = Cache::new("C:/temp/enso-cache").await?;
|
||||
|
@ -13,6 +13,7 @@ use unicase::UniCase;
|
||||
// ==============
|
||||
|
||||
pub mod accessor;
|
||||
pub mod consts;
|
||||
pub mod known;
|
||||
|
||||
|
||||
|
6
build/ci_utils/src/env/accessor.rs
vendored
6
build/ci_utils/src/env/accessor.rs
vendored
@ -104,6 +104,12 @@ impl const From<&'static str> for PathBufVariable {
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<str> for PathBufVariable {
|
||||
fn as_ref(&self) -> &str {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl RawVariable for PathBufVariable {
|
||||
fn name(&self) -> &str {
|
||||
self.0
|
||||
|
5
build/ci_utils/src/env/consts.rs
vendored
Normal file
5
build/ci_utils/src/env/consts.rs
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
/// File extension for Windows shortcut files including the dot.
|
||||
pub const SHORTCUT_EXTENSION: &str = ".lnk";
|
||||
|
||||
/// File suffix for Windows shortcut files without the dot.
|
||||
pub const SHORTCUT_SUFFIX: &str = "lnk";
|
66
build/ci_utils/src/env/known.rs
vendored
66
build/ci_utils/src/env/known.rs
vendored
@ -1,9 +1,20 @@
|
||||
//! Universally known environment variables.
|
||||
|
||||
use crate::prelude::*;
|
||||
|
||||
use crate::define_env_var;
|
||||
use crate::env::accessor::PathBufVariable;
|
||||
use crate::env::accessor::PathLike;
|
||||
|
||||
|
||||
// ==============
|
||||
// === Export ===
|
||||
// ==============
|
||||
|
||||
pub mod cargo;
|
||||
pub mod electron_builder;
|
||||
|
||||
|
||||
|
||||
/// PATH environment variable.
|
||||
///
|
||||
@ -11,13 +22,58 @@ use crate::env::accessor::PathLike;
|
||||
/// executable files.
|
||||
pub const PATH: PathLike = PathLike("PATH");
|
||||
|
||||
/// Windows-specific environment variables.
|
||||
pub mod win {
|
||||
use super::*;
|
||||
|
||||
define_env_var! {
|
||||
/// Per-user custom settings and other information needed by applications.
|
||||
///
|
||||
/// Example: `C:\Users\{username}\AppData\Roaming`
|
||||
APPDATA, PathBuf;
|
||||
|
||||
/// Per-user custom settings and other information needed by applications that do not apply
|
||||
/// when the user roams.
|
||||
LOCALAPPDATA, PathBuf;
|
||||
|
||||
/// Directory where all programs can store their global data.
|
||||
PROGRAMDATA, PathBuf;
|
||||
|
||||
/// Directory where programs are installed (native architecture).
|
||||
PROGRAMFILES, PathBuf;
|
||||
|
||||
/// The user's home directory.
|
||||
USERPROFILE, PathBuf;
|
||||
}
|
||||
|
||||
/// Directory where 32-bit programs are installed.
|
||||
pub const PROGRAMFILES_X86: PathBufVariable = PathBufVariable("ProgramFiles(x86)");
|
||||
|
||||
|
||||
|
||||
/// Directory containing user's Start menu programs shortcuts.
|
||||
pub fn start_menu_programs() -> Result<PathBuf> {
|
||||
Ok(APPDATA.get()?.join_iter(["Microsoft", "Windows", "Start Menu", "Programs"]))
|
||||
}
|
||||
}
|
||||
|
||||
define_env_var! {
|
||||
/// Variable in Unix-like systems that overrides individual `LC_*` settings for locale-specific
|
||||
/// program behavior, such as time formatting (`LC_TIME`), string sorting (`LC_COLLATE`), and
|
||||
/// currency formatting (`LC_MONETARY`). Setting `LC_ALL` ensures uniform application of locale
|
||||
/// settings, commonly utilized in scripting and debugging to maintain consistency irrespective
|
||||
/// of user-specific configurations.
|
||||
/// Overrides individual `LC_*` settings for consistent locale-specific behavior across programs.
|
||||
/// - [`LC_TIME`]: Defines formatting for dates and times.
|
||||
/// - [`LC_COLLATE`]: Determines the sorting order of strings, influencing string comparison operations.
|
||||
/// - [`LC_MONETARY`]: Sets the format for monetary values, including currency symbols and decimal separators.
|
||||
/// Use `LC_ALL` to uniformly apply these settings, which is especially useful in scripts or when debugging
|
||||
/// to avoid locale-related inconsistencies.
|
||||
LC_ALL, String;
|
||||
|
||||
/// Defines formatting for dates and times.
|
||||
LC_TIME, String;
|
||||
|
||||
/// Determines the sorting order of strings.
|
||||
LC_COLLATE, String;
|
||||
|
||||
/// Sets the format for monetary values.
|
||||
LC_MONETARY, String;
|
||||
}
|
||||
|
||||
/// The `C.UTF-8` locale, when used as a value for [`LC_ALL`] or other `LC_*` environment variables
|
||||
|
8
build/ci_utils/src/env/known/cargo.rs
vendored
Normal file
8
build/ci_utils/src/env/known/cargo.rs
vendored
Normal file
@ -0,0 +1,8 @@
|
||||
//! Environment variables known to Cargo.
|
||||
|
||||
|
||||
// ==============
|
||||
// === Export ===
|
||||
// ==============
|
||||
|
||||
pub mod build;
|
12
build/ci_utils/src/env/known/cargo/build.rs
vendored
Normal file
12
build/ci_utils/src/env/known/cargo/build.rs
vendored
Normal file
@ -0,0 +1,12 @@
|
||||
//! Environment variables that are set by Cargo for the build script run.
|
||||
//!
|
||||
//! See [Cargo's documentation](https://doc.rust-lang.org/cargo/reference/environment-variables.html#environment-variables-cargo-sets-for-build-scripts).
|
||||
|
||||
|
||||
|
||||
crate::define_env_var! {
|
||||
/// The folder in which all output and intermediate artifacts should be placed. This folder is
|
||||
/// inside the build directory for the package being built, and it is unique for the package in
|
||||
/// question.
|
||||
OUT_DIR, PathBuf;
|
||||
}
|
161
build/ci_utils/src/env/known/electron_builder.rs
vendored
Normal file
161
build/ci_utils/src/env/known/electron_builder.rs
vendored
Normal file
@ -0,0 +1,161 @@
|
||||
//! Environment variables used by the Electron Builder.
|
||||
|
||||
use crate::prelude::*;
|
||||
|
||||
use crate::define_env_var;
|
||||
|
||||
use base64::Engine;
|
||||
use std::io::Write;
|
||||
|
||||
|
||||
|
||||
define_env_var! {
|
||||
/// The HTTPS link (or base64-encoded data, or file:// link, or local path) to certificate
|
||||
/// (*.p12 or *.pfx file). Shorthand ~/ is supported (home directory).
|
||||
WIN_CSC_LINK, String;
|
||||
|
||||
/// The password to decrypt the certificate given in WIN_CSC_LINK.
|
||||
WIN_CSC_KEY_PASSWORD, String;
|
||||
|
||||
/// The HTTPS link (or base64-encoded data, or file:// link, or local path) to certificate
|
||||
/// (*.p12 or *.pfx file). Shorthand ~/ is supported (home directory).
|
||||
CSC_LINK, String;
|
||||
|
||||
/// The password to decrypt the certificate given in CSC_LINK.
|
||||
CSC_KEY_PASSWORD, String;
|
||||
|
||||
/// The username of apple developer account.
|
||||
APPLEID, String;
|
||||
|
||||
/// The app-specific password (not Apple ID password). See:
|
||||
/// https://support.apple.com/HT204397
|
||||
APPLEIDPASS, String;
|
||||
|
||||
/// Apple Team ID.
|
||||
APPLETEAMID, String;
|
||||
|
||||
/// `true` or `false`. Defaults to `true` — on a macOS development machine valid and
|
||||
/// appropriate identity from your keychain will be automatically used.
|
||||
CSC_IDENTITY_AUTO_DISCOVERY, bool;
|
||||
|
||||
/// Path to the python2 executable, used by electron-builder on macOS to package DMG.
|
||||
PYTHON_PATH, PathBuf;
|
||||
|
||||
/// Note that enabling CSC_FOR_PULL_REQUEST can pose serious security risks. Refer to the
|
||||
/// [CircleCI documentation](https://circleci.com/docs/1.0/fork-pr-builds/) for more
|
||||
/// information. If the project settings contain SSH keys, sensitive environment variables,
|
||||
/// or AWS credentials, and untrusted forks can submit pull requests to your repository, it
|
||||
/// is not recommended to enable this option.
|
||||
CSC_FOR_PULL_REQUEST, bool;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/// CSC (Code Signing Certificate) link.
|
||||
///
|
||||
/// This models the way Electron Builder uses to recieve the certificate file.
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum CscLink {
|
||||
/// Local path to the certificate file.
|
||||
FilePath(PathBuf),
|
||||
/// HTTPS link to the certificate file.
|
||||
Url(Url),
|
||||
/// The certificate file contents.
|
||||
Data(Vec<u8>),
|
||||
}
|
||||
|
||||
impl std::str::FromStr for CscLink {
|
||||
type Err = anyhow::Error;
|
||||
|
||||
#[context("Failed to parse CSC link from '{csc_link}'.")]
|
||||
fn from_str(csc_link: &str) -> Result<Self> {
|
||||
let csc_link = csc_link.trim();
|
||||
if let Some(file_path) = csc_link.strip_prefix("file://") {
|
||||
Ok(Self::FilePath(file_path.into()))
|
||||
} else if let Some(url) = csc_link.strip_prefix("https://") {
|
||||
Ok(Self::Url(url.parse()?))
|
||||
} else if csc_link.len() > 2048 {
|
||||
let contents = base64::engine::general_purpose::STANDARD
|
||||
.decode(csc_link)
|
||||
.context("Failed to decode base64-encoded CSC link.")?;
|
||||
Ok(Self::Data(contents))
|
||||
} else {
|
||||
Ok(Self::FilePath(csc_link.into()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl CscLink {
|
||||
/// Create a new certificate file from the environment variable.
|
||||
pub fn new_from_env() -> Result<Self> {
|
||||
let csc_link = WIN_CSC_LINK.get().or_else(|_| CSC_LINK.get())?;
|
||||
Self::from_str(&csc_link)
|
||||
}
|
||||
}
|
||||
|
||||
/// CSC certificate file to be used for signing the Windows build.
|
||||
#[derive(Debug)]
|
||||
pub enum CodeSigningCertificate {
|
||||
/// Local certificate file.
|
||||
FilePath(PathBuf),
|
||||
/// Temporarily created certificate file.
|
||||
TempFile(tempfile::TempPath),
|
||||
}
|
||||
|
||||
impl AsRef<Path> for CodeSigningCertificate {
|
||||
fn as_ref(&self) -> &Path {
|
||||
match self {
|
||||
Self::FilePath(path) => path.as_ref(),
|
||||
Self::TempFile(path) => path.as_ref(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl CodeSigningCertificate {
|
||||
/// Create a new certificate file from the given link.
|
||||
pub async fn new(link: CscLink) -> Result<Self> {
|
||||
let ret = match link {
|
||||
CscLink::FilePath(path) => Self::FilePath(path),
|
||||
CscLink::Url(url) => {
|
||||
let temp_file = tempfile::NamedTempFile::new()?.into_temp_path();
|
||||
crate::io::web::download_file(url, &temp_file).await?;
|
||||
Self::TempFile(temp_file)
|
||||
}
|
||||
CscLink::Data(contents) => {
|
||||
let temp_file = tempfile::NamedTempFile::new()?;
|
||||
temp_file.as_file().write_all(&contents)?;
|
||||
Self::TempFile(temp_file.into_temp_path())
|
||||
}
|
||||
};
|
||||
Ok(ret)
|
||||
}
|
||||
|
||||
/// Create a new certificate file from the environment variable.
|
||||
pub async fn new_from_env() -> Result<Self> {
|
||||
let csc_link = CscLink::new_from_env()?;
|
||||
Self::new(csc_link).await
|
||||
}
|
||||
}
|
||||
|
||||
/// Data needed to sign the binaries on Windows.
|
||||
#[derive(Debug)]
|
||||
pub struct WindowsSigningCredentials {
|
||||
/// Code signing certificate file.
|
||||
pub certificate: CodeSigningCertificate,
|
||||
/// Password to the certificate.
|
||||
pub password: String,
|
||||
}
|
||||
|
||||
impl WindowsSigningCredentials {
|
||||
/// Create a new certificate file from the environment variable.
|
||||
pub async fn new_from_env() -> Result<Self> {
|
||||
let certificate = CodeSigningCertificate::new_from_env().await?;
|
||||
let password = WIN_CSC_KEY_PASSWORD.get().or_else(|_| CSC_KEY_PASSWORD.get())?;
|
||||
Ok(Self { certificate, password })
|
||||
}
|
||||
|
||||
/// Sign the given binary.
|
||||
pub async fn sign(&self, exe: impl AsRef<Path>) -> Result {
|
||||
crate::programs::signtool::sign(exe, self.certificate.as_ref(), &self.password).await
|
||||
}
|
||||
}
|
@ -11,6 +11,19 @@ pub fn metadata<P: AsRef<Path>>(path: P) -> BoxFuture<'static, Result<std::fs::M
|
||||
tokio::fs::metadata(path).anyhow_err().boxed()
|
||||
}
|
||||
|
||||
|
||||
/// Like [tokio::fs::rename] but with a better error message and static lifetime.
|
||||
pub fn rename<P: AsRef<Path>, Q: AsRef<Path>>(from: P, to: Q) -> BoxFuture<'static, Result<()>> {
|
||||
let from = from.as_ref().to_owned();
|
||||
let to = to.as_ref().to_owned();
|
||||
tokio::fs::rename(from.clone(), to.clone())
|
||||
.with_context(move || {
|
||||
format!("Failed to rename file from: {} to: {}", from.display(), to.display())
|
||||
})
|
||||
.boxed()
|
||||
}
|
||||
|
||||
|
||||
/// See [tokio::fs::symlink_metadata].
|
||||
pub fn symlink_metadata<P: AsRef<Path>>(path: P) -> BoxFuture<'static, Result<std::fs::Metadata>> {
|
||||
let path = path.as_ref().to_owned();
|
||||
|
@ -4,87 +4,72 @@ use tracing_subscriber::prelude::*;
|
||||
use crate::global;
|
||||
|
||||
use std::io;
|
||||
use std::sync::Once;
|
||||
use tracing::span::Attributes;
|
||||
use tracing::subscriber::Interest;
|
||||
use tracing::Event;
|
||||
use tracing::Id;
|
||||
use tracing::Metadata;
|
||||
use tracing::Subscriber;
|
||||
use tracing_subscriber::filter::LevelFilter;
|
||||
use tracing_subscriber::fmt::format::FmtSpan;
|
||||
use tracing_subscriber::registry::LookupSpan;
|
||||
use tracing_subscriber::Layer;
|
||||
use tracing_subscriber::Registry;
|
||||
|
||||
|
||||
|
||||
pub fn is_our_module_path(path: impl AsRef<str>) -> bool {
|
||||
// true
|
||||
["ide_ci::", "enso"].into_iter().any(|prefix| path.as_ref().starts_with(prefix))
|
||||
}
|
||||
|
||||
/// A layer that filters out all spans/events that are not in our module path.
|
||||
#[derive(Clone, Copy, Debug, Display)]
|
||||
pub struct MyLayer;
|
||||
pub struct GlobalFilteringLayer;
|
||||
|
||||
impl<S: Subscriber + Debug + for<'a> LookupSpan<'a>> tracing_subscriber::Layer<S> for MyLayer {
|
||||
impl<S: Subscriber + Debug + for<'a> LookupSpan<'a>> Layer<S> for GlobalFilteringLayer {
|
||||
fn register_callsite(&self, metadata: &'static Metadata<'static>) -> Interest {
|
||||
if metadata.module_path().is_some_and(is_our_module_path) {
|
||||
Interest::always()
|
||||
} else {
|
||||
// dbg!(metadata);
|
||||
Interest::never()
|
||||
}
|
||||
}
|
||||
|
||||
fn on_new_span(
|
||||
&self,
|
||||
_attrs: &Attributes<'_>,
|
||||
_id: &Id,
|
||||
_ctx: tracing_subscriber::layer::Context<'_, S>,
|
||||
) {
|
||||
// let span = ctx.span(id).unwrap();
|
||||
// let bar = crate::global::new_spinner(format!("In span {id:?}: {:?}", span.name()));
|
||||
// span.extensions_mut().insert(bar);
|
||||
// crate::global::println(format!("Create {id:?}"));
|
||||
}
|
||||
fn on_event(&self, _event: &Event<'_>, _ctx: tracing_subscriber::layer::Context<'_, S>) {
|
||||
// tracing_log::dbg!(event);
|
||||
}
|
||||
fn on_enter(&self, _id: &Id, _ctx: tracing_subscriber::layer::Context<'_, S>) {
|
||||
// ide_ci::global::println(format!("Enter {id:?}"));
|
||||
}
|
||||
fn on_exit(&self, _id: &Id, _ctx: tracing_subscriber::layer::Context<'_, S>) {
|
||||
// ide_ci::global::println(format!("Leave {id:?}"));
|
||||
}
|
||||
|
||||
fn on_close(&self, _id: Id, _ctx: tracing_subscriber::layer::Context<'_, S>) {
|
||||
// crate::global::println(format!("Close {id:?}"));
|
||||
}
|
||||
}
|
||||
|
||||
/// Layer that prints logs to stderr.
|
||||
///
|
||||
/// It uses the `ENSO_BUILD_LOG` environment variable to determine the [log filtering](https://docs.rs/tracing-subscriber/latest/tracing_subscriber/filter/struct.EnvFilter.html#directives).
|
||||
pub fn stderr_log_layer<S>() -> impl Layer<S> + Debug
|
||||
where S: Subscriber + for<'a> LookupSpan<'a> + Debug + Send + Sync + 'static {
|
||||
let filter = tracing_subscriber::EnvFilter::builder()
|
||||
.with_env_var("ENSO_BUILD_LOG")
|
||||
.with_default_directive(LevelFilter::TRACE.into())
|
||||
.from_env_lossy();
|
||||
|
||||
let progress_bar_writer = IndicatifWriter::new();
|
||||
tracing_subscriber::fmt::layer()
|
||||
.without_time()
|
||||
.with_ansi(false)
|
||||
.with_span_events(FmtSpan::NEW | FmtSpan::CLOSE)
|
||||
.with_writer(progress_bar_writer)
|
||||
.with_filter(filter)
|
||||
}
|
||||
|
||||
pub fn file_log_layer<S>(file: std::fs::File) -> impl Layer<S> + Debug
|
||||
where S: Subscriber + for<'a> LookupSpan<'a> + Debug + Send + Sync + 'static {
|
||||
tracing_subscriber::fmt::layer()
|
||||
.with_span_events(FmtSpan::NEW | FmtSpan::CLOSE)
|
||||
.with_thread_names(true)
|
||||
.with_writer(file)
|
||||
}
|
||||
|
||||
/// Install global `tracing` subscriber that logs to stderr.
|
||||
///
|
||||
/// Should be called only once, otherwise it will fail.
|
||||
///
|
||||
/// When using this function in unit tests, the result should be ignored, to allow multiple tests
|
||||
/// to be run in a single batch.
|
||||
pub fn setup_logging() -> Result {
|
||||
static GUARD: Once = Once::new();
|
||||
GUARD.call_once(|| {
|
||||
let filter = tracing_subscriber::EnvFilter::builder()
|
||||
.with_env_var("ENSO_BUILD_LOG")
|
||||
.with_default_directive(LevelFilter::TRACE.into())
|
||||
.from_env_lossy();
|
||||
|
||||
let progress_bar_writer = IndicatifWriter::new();
|
||||
|
||||
tracing::subscriber::set_global_default(
|
||||
Registry::default().with(MyLayer).with(
|
||||
tracing_subscriber::fmt::layer()
|
||||
.without_time()
|
||||
.with_span_events(FmtSpan::NEW | FmtSpan::CLOSE)
|
||||
.with_writer(progress_bar_writer)
|
||||
.with_filter(filter),
|
||||
),
|
||||
)
|
||||
.unwrap()
|
||||
});
|
||||
Ok(())
|
||||
let registry = Registry::default().with(GlobalFilteringLayer).with(stderr_log_layer());
|
||||
tracing::subscriber::set_global_default(registry)
|
||||
.context("Failed to set global default subscriber.")
|
||||
}
|
||||
|
||||
|
||||
|
@ -60,7 +60,7 @@ mod tests {
|
||||
#[tokio::test]
|
||||
#[ignore]
|
||||
async fn lookup_dependencies() -> Result {
|
||||
setup_logging()?;
|
||||
setup_logging().ok();
|
||||
vs::apply_dev_environment().await?;
|
||||
let binary = Path::new(
|
||||
r"H:\NBO\enso\built-distribution\enso-engine-2024.1.1-dev-windows-amd64\enso-2024.1.1-dev\component\enso_parser.dll",
|
||||
|
@ -145,6 +145,12 @@ pub trait IsCommandWrapper {
|
||||
self
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
fn raw_arg<S: AsRef<OsStr>>(&mut self, arg: S) -> &mut Self {
|
||||
self.borrow_mut_command().raw_arg(arg);
|
||||
self
|
||||
}
|
||||
|
||||
fn args<I, S>(&mut self, args: I) -> &mut Self
|
||||
where
|
||||
I: IntoIterator<Item = S>,
|
||||
|
@ -6,6 +6,7 @@ pub mod cargo;
|
||||
pub mod cmake;
|
||||
pub mod cmd;
|
||||
pub mod docker;
|
||||
pub mod explorer;
|
||||
pub mod flatc;
|
||||
pub mod git;
|
||||
pub mod go;
|
||||
@ -23,6 +24,7 @@ pub mod sbt;
|
||||
pub mod seven_zip;
|
||||
pub mod sh;
|
||||
pub mod shaderc;
|
||||
pub mod signtool;
|
||||
pub mod spirv_cross;
|
||||
pub mod strip;
|
||||
pub mod tar;
|
||||
|
@ -8,6 +8,7 @@ use crate::program::command::Manipulator;
|
||||
// === Export ===
|
||||
// ==============
|
||||
|
||||
pub mod build;
|
||||
pub mod build_env;
|
||||
pub mod clippy;
|
||||
pub mod fmt;
|
||||
|
22
build/ci_utils/src/programs/cargo/build.rs
Normal file
22
build/ci_utils/src/programs/cargo/build.rs
Normal file
@ -0,0 +1,22 @@
|
||||
//! Directives for Cargo build scripts.
|
||||
//!
|
||||
//! Should be used in `build.rs` files to communicate with Cargo.
|
||||
|
||||
use crate::prelude::*;
|
||||
|
||||
|
||||
|
||||
/// Sets an environment variable.
|
||||
pub fn expose_env_var(var_name: impl AsRef<str>, var_value: impl AsRef<str>) {
|
||||
println!("cargo:rustc-env={}={}", var_name.as_ref(), var_value.as_ref());
|
||||
}
|
||||
|
||||
/// Tells Cargo to rerun the build script if the given file changes.
|
||||
pub fn rerun_if_file_changed(file_path: impl AsRef<Path>) {
|
||||
println!("cargo:rerun-if-changed={}", file_path.as_ref().display());
|
||||
}
|
||||
|
||||
/// Tells Cargo to rerun the build script if the given environment variable changes.
|
||||
pub fn rerun_if_env_changed(var_name: impl AsRef<str>) {
|
||||
println!("cargo:rerun-if-env-changed={}", var_name.as_ref());
|
||||
}
|
@ -690,8 +690,11 @@ mod tests {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
// Don't run by default, user might not have privileges to run docker, or the Docker might not
|
||||
// be configured to run native containers.
|
||||
#[ignore]
|
||||
async fn build_test_linux() -> Result {
|
||||
setup_logging()?;
|
||||
setup_logging().ok();
|
||||
let temp = tempfile::tempdir()?;
|
||||
if Docker.lookup().is_err() {
|
||||
info!("Docker not found, skipping test.");
|
||||
|
43
build/ci_utils/src/programs/explorer.rs
Normal file
43
build/ci_utils/src/programs/explorer.rs
Normal file
@ -0,0 +1,43 @@
|
||||
//! Windows File Explorer-related utilities.
|
||||
//!
|
||||
//! While this module builds cross-platform, it is only useful on Windows.
|
||||
|
||||
use crate::prelude::*;
|
||||
|
||||
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct Explorer;
|
||||
|
||||
impl Program for Explorer {
|
||||
fn executable_name(&self) -> &str {
|
||||
"explorer"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Open the parent folder of the given path in Windows File Explorer and select the given path.
|
||||
// Windows only, due to platform-specific `raw_arg` usage.
|
||||
#[cfg(windows)]
|
||||
pub fn show_selected(path: impl AsRef<Path>) -> Result {
|
||||
let argument = format!(r#"/select,"{}""#, path.as_ref().display());
|
||||
// We use `raw_arg` to avoid escaping the path. The usual quoting rules don't work here.
|
||||
// We ignore the child, as we don't need to wait for it to finish.
|
||||
let _child = Explorer.cmd()?.raw_arg(argument).spawn();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#[allow(unused_imports)]
|
||||
use super::*;
|
||||
|
||||
#[cfg(windows)]
|
||||
#[test]
|
||||
#[ignore]
|
||||
fn show_my_path() {
|
||||
setup_logging().ok();
|
||||
let path = std::env::current_exe().unwrap();
|
||||
show_selected(path).unwrap();
|
||||
}
|
||||
}
|
@ -122,7 +122,7 @@ mod tests {
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_cleaning() -> Result {
|
||||
setup_logging()?;
|
||||
setup_logging().ok();
|
||||
let dir = tempfile::tempdir()?;
|
||||
let dir = dir.path();
|
||||
crate::fs::tokio::reset_dir(dir).await?;
|
||||
|
@ -18,7 +18,7 @@ mod tests {
|
||||
#[tokio::test]
|
||||
#[ignore]
|
||||
async fn call_npx() -> Result {
|
||||
setup_logging()?;
|
||||
setup_logging().ok();
|
||||
Npx.cmd()?.run_ok().await?;
|
||||
Ok(())
|
||||
}
|
||||
|
@ -15,8 +15,8 @@ impl Program for SevenZip {
|
||||
}
|
||||
|
||||
fn default_locations(&self) -> Vec<PathBuf> {
|
||||
if let Ok(program_files) = std::env::var("ProgramFiles") {
|
||||
let path = PathBuf::from(program_files).join("7-Zip");
|
||||
if let Ok(program_files) = crate::env::known::win::PROGRAMFILES.get() {
|
||||
let path = program_files.join("7-Zip");
|
||||
if path.exists() {
|
||||
return vec![path];
|
||||
}
|
||||
|
97
build/ci_utils/src/programs/signtool.rs
Normal file
97
build/ci_utils/src/programs/signtool.rs
Normal file
@ -0,0 +1,97 @@
|
||||
//! Utilities for [`signtool`](SignTool) CLI tool.
|
||||
|
||||
use crate::prelude::*;
|
||||
|
||||
|
||||
|
||||
/// The RFC3161-compliant timestamp server used for signing.
|
||||
pub const TIMESTAMP_SERVER: &str = "http://timestamp.digicert.com";
|
||||
|
||||
/// The hash algorithms that can be used for signing.
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub enum HashAlgorithm {
|
||||
/// SHA-1 hash algorithm.
|
||||
SHA1,
|
||||
/// SHA-256 hash algorithm.
|
||||
SHA256,
|
||||
}
|
||||
|
||||
/// Compatible with format expected by the `signtool` utility CLI.
|
||||
impl AsRef<str> for HashAlgorithm {
|
||||
fn as_ref(&self) -> &str {
|
||||
match self {
|
||||
HashAlgorithm::SHA1 => "SHA1",
|
||||
HashAlgorithm::SHA256 => "SHA256",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<OsStr> for HashAlgorithm {
|
||||
fn as_ref(&self) -> &OsStr {
|
||||
AsRef::<str>::as_ref(self).as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
/// SignTool utility, being part of the Windows SDK.
|
||||
///
|
||||
/// `signtool` can be used to sign executables, verify signatures, or timestamp files.
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct SignTool;
|
||||
|
||||
impl Program for SignTool {
|
||||
fn executable_name(&self) -> &str {
|
||||
"signtool"
|
||||
}
|
||||
|
||||
fn default_locations(&self) -> Vec<PathBuf> {
|
||||
if let Ok(sdk_dir) = locate_windows_sdk() {
|
||||
vec![sdk_dir.join("App Certification Kit")]
|
||||
} else {
|
||||
Vec::new()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Heuristically locate the Windows SDK.
|
||||
///
|
||||
/// The `signtool` utility is part of the Windows SDK, so we need to locate it to use it.
|
||||
pub fn locate_windows_sdk() -> Result<PathBuf> {
|
||||
let program_files = crate::env::known::win::PROGRAMFILES_X86.get()?;
|
||||
let sdk_dir = program_files.join_iter(["Windows Kits", "10"]);
|
||||
// TODO: If we ever want anything more fancy, we should use the `InstallationFolder` key at
|
||||
// `HKEY_LOCAL_MACHINE\SOFTWARE\WOW6432Node\Microsoft\Microsoft SDKs\Windows\v10.0`
|
||||
// For now we don't need it, and using `winreg` crate is too much trouble cross-platform.
|
||||
if sdk_dir.exists() {
|
||||
Ok(sdk_dir)
|
||||
} else {
|
||||
bail!("Windows SDK not found!")
|
||||
}
|
||||
}
|
||||
|
||||
/// Sign the given executable with the given certificate.
|
||||
///
|
||||
/// The hash algorithm used for signing is SHA-256, as SHA-1 is deprecated and not trusted.
|
||||
/// The only reason not use SHA-256 would be to target OS versions older than Windows XP SP3, which
|
||||
/// are not supported anyway.
|
||||
pub async fn sign(
|
||||
exe: impl AsRef<Path>,
|
||||
cert: impl AsRef<Path>,
|
||||
password: impl AsRef<str>,
|
||||
) -> Result {
|
||||
SignTool
|
||||
.cmd()?
|
||||
.arg("sign")
|
||||
.arg("/f")
|
||||
.arg(cert.as_ref())
|
||||
.arg("/p")
|
||||
.arg(password.as_ref())
|
||||
.arg("/fd")
|
||||
.arg(HashAlgorithm::SHA256)
|
||||
.arg("/tr")
|
||||
.arg(TIMESTAMP_SERVER)
|
||||
.arg("/td")
|
||||
.arg(HashAlgorithm::SHA256)
|
||||
.arg(exe.as_ref())
|
||||
.run_ok()
|
||||
.await
|
||||
}
|
@ -283,7 +283,7 @@ pub mod tests {
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_directory_packing() -> Result {
|
||||
setup_logging()?;
|
||||
setup_logging().ok();
|
||||
let archive_temp = tempfile::tempdir()?;
|
||||
let archive_path = archive_temp.path().join("archive.tar.gz");
|
||||
|
||||
|
@ -47,7 +47,7 @@ mod tests {
|
||||
#[tokio::test]
|
||||
#[ignore]
|
||||
async fn test_listing_dlls() -> Result {
|
||||
setup_logging()?;
|
||||
setup_logging().ok();
|
||||
apply_dev_environment().await?;
|
||||
let dlls = list_crt_dlls(vs::Platforms::local()?).await?;
|
||||
dbg!(&dlls);
|
||||
|
@ -21,7 +21,7 @@ octocrab = { workspace = true }
|
||||
tempfile = "3.2.0"
|
||||
tokio = { workspace = true }
|
||||
toml = "0.5.9"
|
||||
tracing = { version = "0.1.37" }
|
||||
tracing = { workspace = true }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
46
build/install/Cargo.toml
Normal file
46
build/install/Cargo.toml
Normal file
@ -0,0 +1,46 @@
|
||||
[package]
|
||||
name = "enso-install"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
chrono = { workspace = true }
|
||||
dirs = { workspace = true }
|
||||
enso-build-base = { path = "../base" }
|
||||
enso-install-config = { path = "config" }
|
||||
flate2 = { workspace = true }
|
||||
ide-ci = { path = "../ci_utils" }
|
||||
indicatif = { workspace = true }
|
||||
named-lock = "0.4.1"
|
||||
self-replace = "1.3.7"
|
||||
serde_json = { workspace = true }
|
||||
strum = { workspace = true }
|
||||
sysinfo = { workspace = true }
|
||||
tar = { workspace = true }
|
||||
tokio = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
tracing-subscriber = { workspace = true }
|
||||
windows = { version = "0.53.0", features = [
|
||||
"Win32",
|
||||
"Win32_UI",
|
||||
"Win32_UI_Shell",
|
||||
"Win32_System",
|
||||
"Win32_System_LibraryLoader",
|
||||
"Win32_Foundation",
|
||||
"Win32_System_Com",
|
||||
] }
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
mslnk = "0.1.8"
|
||||
native-windows-gui = { workspace = true }
|
||||
winreg = { workspace = true }
|
||||
|
||||
[build-dependencies]
|
||||
embed-resource = "2.4.0"
|
||||
ide-ci = { path = "../ci_utils" }
|
||||
enso-install-config = { path = "config" }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
93
build/install/build.rs
Normal file
93
build/install/build.rs
Normal file
@ -0,0 +1,93 @@
|
||||
//! The build script of crate that is dependency of both the installer and the uninstaller.
|
||||
//!
|
||||
//! Currently, it is used to include the resources (icons, manifests, version information) into the
|
||||
//! installer and uninstaller binaries.
|
||||
|
||||
use enso_install_config::prelude::*;
|
||||
|
||||
use enso_install_config::embed_resource_from_file;
|
||||
use enso_install_config::sanitize_and_expose_electron_builder_config;
|
||||
use enso_install_config::ResourceType;
|
||||
use enso_install_config::ENSO_ICON_ID;
|
||||
use ide_ci::programs::cargo::build_env::OUT_DIR;
|
||||
|
||||
|
||||
|
||||
/// Build-script-time function that tries to embed the icon into the binary.
|
||||
///
|
||||
/// Works only on Windows, on other platforms this is effectively a no-op.
|
||||
fn try_embedding_icon() -> Result {
|
||||
let config = enso_install_config::electron_builder_config_from_env()?;
|
||||
embed_resource_from_file(ENSO_ICON_ID, ResourceType::Icon, &config.win.icon)
|
||||
}
|
||||
|
||||
fn main() {
|
||||
setup_logging().ok();
|
||||
// Ignore error, not compiling the icon is not a big deal, especially if we compile to check, to
|
||||
// not generate final package. (Obtaining icon is a bit of a hassle, as it is generated
|
||||
// temporarily by the enso-build.)
|
||||
if let Err(err) = try_embedding_icon() {
|
||||
// We do not use `cargo:warning` here, as we do not want to pollute the output if the icon
|
||||
// is not available. Still, to enable debugging, we print to stderr, which is captured by
|
||||
// the cargo and stored in `target/debug/build/<pkg>/output`.
|
||||
eprintln!("Failed to embed icon: {err:?}");
|
||||
}
|
||||
|
||||
let config = sanitize_and_expose_electron_builder_config();
|
||||
if let Ok(config) = config {
|
||||
// Content on the rc file with the version information.
|
||||
let version_rc = format!(
|
||||
r#"
|
||||
#include "winres.h"
|
||||
#define IDS_VERSION_INFO 1
|
||||
|
||||
VS_VERSION_INFO VERSIONINFO
|
||||
FILEVERSION {major},{minor},{patch},0
|
||||
PRODUCTVERSION {major},{minor},{patch},0
|
||||
FILEOS 0x4
|
||||
FILETYPE 0x1
|
||||
BEGIN
|
||||
BLOCK "StringFileInfo"
|
||||
BEGIN
|
||||
BLOCK "040904e4"
|
||||
BEGIN
|
||||
VALUE "CompanyName", "{company}"
|
||||
VALUE "FileVersion", "{version}"
|
||||
VALUE "LegalCopyright", "{copyright}"
|
||||
VALUE "ProductName", "{name}"
|
||||
VALUE "ProductVersion", "{version}"
|
||||
END
|
||||
END
|
||||
BLOCK "VarFileInfo"
|
||||
BEGIN
|
||||
VALUE "Translation", 0x409, 1252
|
||||
END
|
||||
END
|
||||
"#,
|
||||
major = config.extra_metadata.version.major,
|
||||
minor = config.extra_metadata.version.minor,
|
||||
patch = config.extra_metadata.version.patch,
|
||||
company = config.copyright,
|
||||
name = config.product_name,
|
||||
version = config.extra_metadata.version,
|
||||
copyright = config.copyright
|
||||
);
|
||||
let path = OUT_DIR.get().unwrap().join("version.rc");
|
||||
ide_ci::fs::write_if_different(&path, version_rc).unwrap();
|
||||
embed_resource::compile(&path, embed_resource::NONE);
|
||||
}
|
||||
|
||||
// Embed the manifest file.
|
||||
// Necessary to avoid the issue with `GetWindowSubclass`. See:
|
||||
// * https://github.com/gabdube/native-windows-gui/issues/251
|
||||
// * https://github.com/microsoft/windows-rs/issues/1294
|
||||
let manifest_path = Path::new("enso-install.manifest");
|
||||
assert!(manifest_path.exists(), "Manifest file does not exist: {}", manifest_path.display());
|
||||
let rc_path = OUT_DIR.get().unwrap().join("manifest.rc");
|
||||
ide_ci::fs::write_if_different(
|
||||
&rc_path,
|
||||
format!("#define RT_MANIFEST 24\n1 RT_MANIFEST \"{}\"", manifest_path.display()),
|
||||
)
|
||||
.unwrap();
|
||||
embed_resource::compile(&rc_path, embed_resource::NONE);
|
||||
}
|
16
build/install/config/Cargo.toml
Normal file
16
build/install/config/Cargo.toml
Normal file
@ -0,0 +1,16 @@
|
||||
[package]
|
||||
name = "enso-install-config"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
embed-resource = "2.4.0"
|
||||
enso-build-base = { path = "../../base" }
|
||||
ide-ci = { path = "../../ci_utils" }
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
tempfile = "3.2.0"
|
||||
walkdir = { workspace = true }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
104
build/install/config/src/bundler.rs
Normal file
104
build/install/config/src/bundler.rs
Normal file
@ -0,0 +1,104 @@
|
||||
//! Utilities for building the Windows installer/uninstaller for the Enso application.
|
||||
//!
|
||||
//! See the [`bundle`] function as the main entry point.
|
||||
|
||||
use crate::prelude::*;
|
||||
|
||||
use crate::INSTALLER_NAME;
|
||||
use crate::UNINSTALLER_NAME;
|
||||
|
||||
use ide_ci::env::known::electron_builder::WindowsSigningCredentials;
|
||||
use ide_ci::programs::cargo;
|
||||
use ide_ci::programs::Cargo;
|
||||
|
||||
|
||||
|
||||
/// Input necessary to generate a Windows installer from unpacked Electron application bundle.
|
||||
#[derive(Debug)]
|
||||
pub struct Config {
|
||||
/// File to the JSON file containing the Electron Builder configuration.
|
||||
pub electron_builder_config: PathBuf,
|
||||
/// Path to the directory containing the unpacked Electron application bundle.
|
||||
///
|
||||
/// It is obtained by running the `electron-builder` with the `--dir` option.
|
||||
pub unpacked_electron_bundle: PathBuf,
|
||||
/// Path to the root of the repository.
|
||||
pub repo_root: PathBuf,
|
||||
/// Path where the generated installer should be saved.
|
||||
pub output_file: PathBuf,
|
||||
/// Path to the directory where intermediate files should be stored.
|
||||
pub intermediate_dir: PathBuf,
|
||||
/// Certificate used to sign the installer and uninstaller.
|
||||
///
|
||||
/// If `None`, the installer and uninstaller will not be signed.
|
||||
pub certificate: Option<WindowsSigningCredentials>,
|
||||
}
|
||||
|
||||
/// Builds a package using Cargo and optionally signs it with a certificate.
|
||||
pub async fn build_package(
|
||||
crate_name: &str,
|
||||
output_file: &Path,
|
||||
certificate: Option<&WindowsSigningCredentials>,
|
||||
prepare_env: impl FnOnce(&mut Command) -> Result<&mut Command>,
|
||||
) -> Result {
|
||||
let temp_dir = tempfile::tempdir()?;
|
||||
let mut cmd = Cargo.cmd()?;
|
||||
prepare_env(&mut cmd)?;
|
||||
cmd.apply(&cargo::Command::Build)
|
||||
.arg("--release")
|
||||
.arg("--package")
|
||||
.arg(crate_name)
|
||||
.arg("-Z")
|
||||
.arg("unstable-options")
|
||||
.arg("--out-dir")
|
||||
.arg(temp_dir.path())
|
||||
.run_ok()
|
||||
.await?;
|
||||
|
||||
let built_exe = temp_dir.path().join(crate_name).with_executable_extension();
|
||||
if let Some(certificate) = certificate {
|
||||
certificate.sign(&built_exe).await?;
|
||||
}
|
||||
ide_ci::fs::tokio::copy(&built_exe, output_file).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Package the Enso unpacked Electron application bundle (electron-builder's output) into an
|
||||
/// installer.
|
||||
///
|
||||
/// First, the uninstaller is built and signed. Then, the payload is prepared and the installer is
|
||||
/// built and signed.
|
||||
pub async fn bundle(config: Config) -> Result {
|
||||
let Config {
|
||||
electron_builder_config,
|
||||
unpacked_electron_bundle,
|
||||
repo_root,
|
||||
output_file,
|
||||
intermediate_dir,
|
||||
certificate,
|
||||
} = config;
|
||||
|
||||
// The uninstaller must be built first, as it is part of the distribution package.
|
||||
let uninstaller_path =
|
||||
unpacked_electron_bundle.join(UNINSTALLER_NAME).with_executable_extension();
|
||||
build_package(UNINSTALLER_NAME, &uninstaller_path, certificate.as_ref(), |cmd| {
|
||||
cmd.current_dir(&repo_root);
|
||||
cmd.set_env(crate::ENSO_BUILD_ELECTRON_BUILDER_CONFIG, &electron_builder_config)
|
||||
})
|
||||
.await?;
|
||||
|
||||
// Prepare the archive payload for the installer.
|
||||
let archive_path = intermediate_dir.join("payload.tar.gz");
|
||||
let metadata_path = intermediate_dir.join("payload.json");
|
||||
crate::payload::prepare_payload(&unpacked_electron_bundle, &archive_path, &metadata_path)
|
||||
.await?;
|
||||
|
||||
// Finally build the installer.
|
||||
build_package(INSTALLER_NAME, &output_file, certificate.as_ref(), |cmd| {
|
||||
cmd.current_dir(&repo_root)
|
||||
.set_env(crate::ENSO_INSTALL_ARCHIVE_PATH, &archive_path)?
|
||||
.set_env(crate::ENSO_INSTALL_METADATA_PATH, &metadata_path)?
|
||||
.set_env(crate::ENSO_BUILD_ELECTRON_BUILDER_CONFIG, &electron_builder_config)
|
||||
})
|
||||
.await
|
||||
}
|
133
build/install/config/src/electron_builder.rs
Normal file
133
build/install/config/src/electron_builder.rs
Normal file
@ -0,0 +1,133 @@
|
||||
//! Rust representation of the `electron-builder` configuration subset.
|
||||
|
||||
use crate::prelude::*;
|
||||
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
|
||||
|
||||
|
||||
/// Additional configuration needed by the installer that is not part of the standard
|
||||
/// `electron-builder` configuration.
|
||||
///
|
||||
/// This configuration is received through `installer` field in the `extraMetadata`.
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct InstallerConfig {
|
||||
/// The company name to be used in the installer. Example: `"New Byte Order sp. z o.o."`.
|
||||
pub publisher: String,
|
||||
|
||||
/// Extended file association configuration.
|
||||
pub file_associations: Vec<ExtendedFileAssociation>,
|
||||
}
|
||||
|
||||
/// A subset of the configuration options available in the `electron-builder` configuration.
|
||||
///
|
||||
/// Note that some fields should not be included here (like code signing options) as this
|
||||
/// configuration might end up being compiled into and shipped with the application installer.
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Config {
|
||||
/// The application ID, usually in reverse domain name notation. Example: `"org.enso"`.
|
||||
pub app_id: String,
|
||||
|
||||
/// The name of the product. Example: `"Enso"`.
|
||||
pub product_name: String,
|
||||
|
||||
/// Extra metadata for the application, like version. Example: `{"version": "2023.2.1-dev"}`.
|
||||
pub extra_metadata: ExtraMetadata,
|
||||
|
||||
/// Copyright notice of the application. Example: `"Copyright © 2023 New Byte Order sp. z
|
||||
/// o.o."`.
|
||||
pub copyright: String,
|
||||
|
||||
/// Pattern for naming artifact files. Example: `"enso-${os}-2023.2.1-dev.${ext}"`.
|
||||
pub artifact_name: String,
|
||||
|
||||
/// Custom protocol schemes that the application handles. Example: `[{ "name": "Enso url",
|
||||
/// "schemes": ["enso"], "role": "Editor" }]`.
|
||||
pub protocols: Vec<Protocol>,
|
||||
|
||||
/// Configuration specific to Windows builds.
|
||||
pub win: WinConfig,
|
||||
|
||||
/// File associations for the application. Example: `[{ "ext": "enso", "name": "Enso Source
|
||||
/// File", "role": "Editor" }, {...}]`.
|
||||
///
|
||||
/// Installer uses extended version of this configuration, see [`InstallerConfig`].
|
||||
pub file_associations: Vec<FileAssociation>,
|
||||
|
||||
/// Configuration for the output directories. Example: `{ "output":
|
||||
/// "/home/mwu/Desktop/enso/dist/ide2" }`.
|
||||
pub directories: Directories,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ExtraMetadata {
|
||||
/// Version of the application. Example: `"2023.2.1-dev"`.
|
||||
pub version: Version,
|
||||
|
||||
/// Additional configuration needed by the installer.
|
||||
pub installer: InstallerConfig,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Protocol {
|
||||
/// Name of the protocol. Example: `"Enso url"`.
|
||||
pub name: String,
|
||||
|
||||
/// Schemes associated with the protocol. Example: `["enso"]`.
|
||||
pub schemes: Vec<String>,
|
||||
|
||||
/// Role of the application in handling the protocol. Example: `"Editor"`.
|
||||
pub role: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct WinConfig {
|
||||
/// Path to the icon file for Windows. Example: `"/tmp/.tmpMPYWpz/icon.ico"`.
|
||||
pub icon: PathBuf,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct FileAssociation {
|
||||
/// File extension to associate. Example: `"enso"`.
|
||||
pub ext: String,
|
||||
|
||||
/// Name of the file type. Example: `"Enso Source File"`.
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Directories {
|
||||
/// Output directory for the build. Example: `"/home/mwu/Desktop/enso/dist/ide2"`.
|
||||
pub output: PathBuf,
|
||||
}
|
||||
|
||||
/// [`FileAssociation`] with additional fields for MIME type and ProgID.
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ExtendedFileAssociation {
|
||||
/// The [programmatic identifier](https://docs.microsoft.com/en-us/windows/win32/shell/fa-progids) of the file type. Example: `"Enso.Source"`.
|
||||
pub prog_id: String,
|
||||
|
||||
/// MIME type of the file type. Example: `"text/plain"`.
|
||||
pub mime_type: String,
|
||||
|
||||
/// Standard `electron-builder` file association configuration.
|
||||
#[serde(flatten)]
|
||||
pub base: FileAssociation,
|
||||
}
|
||||
|
||||
impl Deref for ExtendedFileAssociation {
|
||||
type Target = FileAssociation;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.base
|
||||
}
|
||||
}
|
171
build/install/config/src/lib.rs
Normal file
171
build/install/config/src/lib.rs
Normal file
@ -0,0 +1,171 @@
|
||||
//! A crate that provides utilities for building the Enso installer. It is used by:
|
||||
//! * the build script (`./run`) - for [creating the installer bundle](bundler::bundle) (and
|
||||
//! providing the necessary resources);
|
||||
//! * the installer's and uninstaller's `build.rs` build scripts - for embedding the necessary
|
||||
//! resources;
|
||||
//! * the installer's and uninstaller's runtime code - for accessing the configuration. Note that
|
||||
//! resource access in not part of this crate, as it is Windows-specific.
|
||||
|
||||
|
||||
|
||||
pub mod prelude {
|
||||
pub use ide_ci::prelude::*;
|
||||
}
|
||||
|
||||
pub mod bundler;
|
||||
pub mod electron_builder;
|
||||
pub mod payload;
|
||||
|
||||
use prelude::*;
|
||||
|
||||
use ide_ci::define_env_var;
|
||||
use ide_ci::env::accessor::PathBufVariable;
|
||||
use ide_ci::env::known::cargo::build::OUT_DIR;
|
||||
use ide_ci::programs::cargo;
|
||||
|
||||
/// The filename stem of the installer executable — and the crate name.
|
||||
pub const INSTALLER_NAME: &str = "enso-installer";
|
||||
|
||||
/// The filename stem of the uninstaller executable — and the crate name.
|
||||
pub const UNINSTALLER_NAME: &str = "enso-uninstaller";
|
||||
|
||||
define_env_var! {
|
||||
/// Path to the JSON file containing the Electron Builder configuration.
|
||||
///
|
||||
/// Unlike [`ENSO_INSTALL_ELECTRON_BUILDER_CONFIG`] this is a full dump of the configuration.
|
||||
/// Provided by the enso-build to the `enso-install`'s `build.rs`.
|
||||
ENSO_BUILD_ELECTRON_BUILDER_CONFIG, PathBuf;
|
||||
|
||||
/// Path to the JSON file containing the Electron Builder configuration.
|
||||
///
|
||||
/// This file is sanitized by the `enso-install`'s `build.rs` to contain only the necessary
|
||||
/// information, as defined by the [`electron_builder::Config`] type.
|
||||
///
|
||||
/// In general, the installer/unistaller `build.rs` should set this variable, so it can be
|
||||
/// embedded into the relevant binary.
|
||||
ENSO_INSTALL_ELECTRON_BUILDER_CONFIG, PathBuf;
|
||||
|
||||
/// The path to the `tar.gz` archive containing the Enso IDE payload. Provided by the
|
||||
/// enso-build to the enso-installer's `build.rs`, which embeds it into the installer's binary.
|
||||
ENSO_INSTALL_ARCHIVE_PATH, PathBuf;
|
||||
|
||||
/// The path to the JSON file containing the metadata of the Enso IDE payload.
|
||||
///
|
||||
/// The metadata is modeled by the [`crate::payload::Metadata`] struct.
|
||||
ENSO_INSTALL_METADATA_PATH, PathBuf;
|
||||
}
|
||||
|
||||
/// Build-time script (build.rs) that retrieves the electron-builder configuration from the
|
||||
/// file designated by the [`ENSO_BUILD_ELECTRON_BUILDER_CONFIG`] environment variable.
|
||||
///
|
||||
/// This function is intended to be used by the installer/uninstaller's `build.rs`.
|
||||
pub fn electron_builder_config_from_env() -> Result<electron_builder::Config> {
|
||||
let config_path = ENSO_BUILD_ELECTRON_BUILDER_CONFIG.get()?;
|
||||
cargo::build::rerun_if_env_changed(ENSO_BUILD_ELECTRON_BUILDER_CONFIG);
|
||||
cargo::build::rerun_if_file_changed(&config_path);
|
||||
ide_ci::fs::read_json(config_path)
|
||||
}
|
||||
|
||||
/// Location where the sanitized electron-builder configuration is placed.
|
||||
///
|
||||
/// Should be used only `build.rs`-time.
|
||||
///
|
||||
/// Runtime installer-dependent crates should embed the file.
|
||||
pub fn sanitized_electron_builder_config_path() -> Result<PathBuf> {
|
||||
Ok(OUT_DIR.get()?.join("electron-builder-config.json"))
|
||||
}
|
||||
|
||||
/// Place electron-builder configuration under the output directory.
|
||||
///
|
||||
/// The file is taken from [`ENSO_BUILD_ELECTRON_BUILDER_CONFIG`], sanitized and placed under the
|
||||
/// output directory. The resulting location will be exposed to the build through the environment
|
||||
/// variable [`ENSO_INSTALL_ELECTRON_BUILDER_CONFIG`].
|
||||
///
|
||||
/// This function is intended to be used by the installer/uninstaller's `build.rs`.
|
||||
///
|
||||
/// Returns the parsed configuration.
|
||||
pub fn sanitize_and_expose_electron_builder_config() -> Result<electron_builder::Config> {
|
||||
// We sanitize by parsing into our structure (with only the fields we need) and then dumping
|
||||
// back to JSON.
|
||||
let config = electron_builder_config_from_env();
|
||||
let out_config_path = sanitized_electron_builder_config_path()?;
|
||||
if let Ok(config) = &config {
|
||||
let json_text = serde_json::to_string_pretty(config)?;
|
||||
ide_ci::fs::write_if_different(&out_config_path, json_text)?;
|
||||
} else {
|
||||
// We write dummy. This is to avoid the build script failing if the config is not available.
|
||||
// This allows checking if the installer compiles without involving any electron-builder
|
||||
// configuration.
|
||||
ide_ci::fs::write_if_different(&out_config_path, "")?;
|
||||
}
|
||||
cargo::build::expose_env_var(ENSO_INSTALL_ELECTRON_BUILDER_CONFIG, out_config_path.as_str());
|
||||
ENSO_INSTALL_ELECTRON_BUILDER_CONFIG.set(&out_config_path)?;
|
||||
config
|
||||
}
|
||||
|
||||
/// The resource (RCDATA) ID of the Enso installer payload.
|
||||
///
|
||||
/// The payload is a `tar.gz` archive containing the Enso IDE.
|
||||
pub const INSTALLER_PAYLOAD_ID: &str = "INSTALLER_PAYLOAD";
|
||||
|
||||
/// Identifier for the Enso icon resource.
|
||||
pub const ENSO_ICON_ID: &str = "ENSO_ICON_ID";
|
||||
|
||||
|
||||
/// Resources that can be embedded into the binary.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum ResourceType {
|
||||
/// Icon resource (`.ico` file).
|
||||
///
|
||||
/// Windows will automatically use the first embedded icon as the icon for the binary.
|
||||
Icon,
|
||||
/// Arbitrary binary data.
|
||||
Binary,
|
||||
}
|
||||
|
||||
impl Display for ResourceType {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
ResourceType::Icon => write!(f, "ICON"),
|
||||
ResourceType::Binary => write!(f, "RCDATA"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Embeds a file as a resource into the binary.
|
||||
///
|
||||
/// This function is intended to be used by the installer/uninstaller's `build.rs`.
|
||||
pub fn embed_resource_from_file(
|
||||
resource_id: &str,
|
||||
resource_type: ResourceType,
|
||||
resource_path: &Path,
|
||||
) -> Result {
|
||||
let rc_file = OUT_DIR.get().unwrap().join(resource_id).with_extension("rc");
|
||||
cargo::build::rerun_if_env_changed(rc_file.as_str());
|
||||
// We need to either replace backslashes with forward slashes or escape them, as RC file is
|
||||
// kinda-compiled. The former is easier.
|
||||
let sanitized_path = resource_path.to_str().unwrap().replace('\\', "/");
|
||||
cargo::build::rerun_if_file_changed(&sanitized_path);
|
||||
let contents = format!(r#"{resource_id} {resource_type} "{sanitized_path}""#);
|
||||
ide_ci::fs::write_if_different(&rc_file, contents)?;
|
||||
embed_resource::compile(&rc_file, embed_resource::NONE);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
/// Embeds a resource using a path from an environment variable.
|
||||
///
|
||||
/// It should be preferred over [`embed_resource_from_file`] when the path to the resource is stored
|
||||
/// in an environment variable. The function will automatically rerun the build script if the
|
||||
/// environment variable changes.
|
||||
///
|
||||
/// This function is intended to be used by the installer/uninstaller's `build.rs`.
|
||||
pub fn embed_resource_from_env(
|
||||
resource_id: &str,
|
||||
resource_type: ResourceType,
|
||||
env_var: &PathBufVariable,
|
||||
) -> Result {
|
||||
cargo::build::rerun_if_env_changed(env_var);
|
||||
embed_resource_from_file(resource_id, resource_type, &env_var.get()?)
|
||||
}
|
50
build/install/config/src/payload.rs
Normal file
50
build/install/config/src/payload.rs
Normal file
@ -0,0 +1,50 @@
|
||||
//! Installer has compiled-in binary payload and metadata about it.
|
||||
|
||||
use crate::prelude::*;
|
||||
|
||||
|
||||
|
||||
/// Information about the archive payload of the installer.
|
||||
///
|
||||
/// This information is used to display progress information to the user. While it could be
|
||||
/// generated at runtime by inspecting the archive, it is more efficient to generate it at build
|
||||
/// time and embed it into the installer binary.
|
||||
#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
|
||||
pub struct Metadata {
|
||||
/// Number of files in the archive.
|
||||
pub total_files: u64,
|
||||
/// Total size of the extracted files in bytes.
|
||||
pub total_bytes: u64,
|
||||
}
|
||||
|
||||
impl Metadata {
|
||||
/// Scans the given directory and calculates the payload information.
|
||||
pub fn from_directory(unpacked_directory: &Path) -> Result<Self> {
|
||||
let mut total_files = 0;
|
||||
let mut total_bytes = 0;
|
||||
for entry in walkdir::WalkDir::new(unpacked_directory) {
|
||||
let entry = entry?;
|
||||
let metadata = entry.metadata()?;
|
||||
total_files += 1;
|
||||
// We treat directories as empty files.
|
||||
total_bytes += if metadata.is_dir() { 0 } else { metadata.len() };
|
||||
}
|
||||
Ok(Self { total_files, total_bytes })
|
||||
}
|
||||
}
|
||||
|
||||
/// Take the electron-builder output and prepare the payload files for the installer.
|
||||
///
|
||||
/// These files need to be provided to the installer at build time, so they can be embedded into
|
||||
/// the installer binary
|
||||
pub async fn prepare_payload(
|
||||
unpacked_directory: &Path,
|
||||
output_archive: &Path,
|
||||
output_metadata: &Path,
|
||||
) -> Result {
|
||||
let metadata = Metadata::from_directory(unpacked_directory)?;
|
||||
ide_ci::archive::compress_directory_contents(&output_archive, &unpacked_directory).await?;
|
||||
let metadata_json = serde_json::to_string_pretty(&metadata)?;
|
||||
ide_ci::fs::write_if_different(output_metadata, metadata_json)?;
|
||||
Ok(())
|
||||
}
|
39
build/install/enso-install.manifest
Normal file
39
build/install/enso-install.manifest
Normal file
@ -0,0 +1,39 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0" xmlns:asmv3="urn:schemas-microsoft-com:asm.v3">
|
||||
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v3">
|
||||
<security>
|
||||
<requestedPrivileges>
|
||||
<requestedExecutionLevel level="asInvoker" uiAccess="false"/>
|
||||
</requestedPrivileges>
|
||||
</security>
|
||||
</trustInfo>
|
||||
<assemblyIdentity
|
||||
version="1.0.0.0"
|
||||
processorArchitecture="*"
|
||||
name="app"
|
||||
type="win32"
|
||||
/>
|
||||
<asmv3:application>
|
||||
<asmv3:windowsSettings xmlns="http://schemas.microsoft.com/SMI/2019/WindowsSettings">
|
||||
<activeCodePage>UTF-8</activeCodePage>
|
||||
</asmv3:windowsSettings>
|
||||
</asmv3:application>
|
||||
<asmv3:application>
|
||||
<asmv3:windowsSettings xmlns:ws2="http://schemas.microsoft.com/SMI/2016/WindowsSettings">
|
||||
<ws2:longPathAware>true</ws2:longPathAware>
|
||||
</asmv3:windowsSettings>
|
||||
</asmv3:application>
|
||||
<dependency>
|
||||
<dependentAssembly>
|
||||
<!-- Needed for GetWindowSubclass entry point, see https://github.com/gabdube/native-windows-gui/issues/251. -->
|
||||
<assemblyIdentity
|
||||
type="win32"
|
||||
name="Microsoft.Windows.Common-Controls"
|
||||
version="6.0.0.0"
|
||||
processorArchitecture="*"
|
||||
publicKeyToken="6595b64144ccf1df"
|
||||
language="*"
|
||||
/>
|
||||
</dependentAssembly>
|
||||
</dependency>
|
||||
</assembly>
|
35
build/install/installer/Cargo.toml
Normal file
35
build/install/installer/Cargo.toml
Normal file
@ -0,0 +1,35 @@
|
||||
[package]
|
||||
name = "enso-installer"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
anyhow = { workspace = true }
|
||||
byte-unit = { workspace = true }
|
||||
chrono = { workspace = true }
|
||||
enso-build-base = { path = "../../base" }
|
||||
enso-install = { path = ".." }
|
||||
enso-install-config = { path = "../config" }
|
||||
flate2 = { workspace = true }
|
||||
ide-ci = { path = "../../ci_utils" }
|
||||
serde_json = { workspace = true }
|
||||
sysinfo = { workspace = true }
|
||||
tar = { workspace = true }
|
||||
tokio = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
tracing-subscriber = { workspace = true }
|
||||
windows = { workspace = true }
|
||||
winapi = "0.3"
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
native-windows-gui = { workspace = true }
|
||||
|
||||
[build-dependencies]
|
||||
embed-resource = "2.4.0"
|
||||
ide-ci = { path = "../../ci_utils" }
|
||||
enso-install-config = { path = "../config" }
|
||||
flate2 = { workspace = true }
|
||||
tar = { workspace = true }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
46
build/install/installer/build.rs
Normal file
46
build/install/installer/build.rs
Normal file
@ -0,0 +1,46 @@
|
||||
//! The build script of the installer.
|
||||
//!
|
||||
//! It is used to compile in the binary payload and metadata about it into the installer binary.
|
||||
//!
|
||||
//! Note that other resources (icons, manifests, version information) are already included in the
|
||||
//! installer binary by the `enso-install` library crate.
|
||||
|
||||
use ide_ci::prelude::*;
|
||||
|
||||
use enso_install_config::sanitize_and_expose_electron_builder_config;
|
||||
use enso_install_config::ENSO_INSTALL_ARCHIVE_PATH;
|
||||
use enso_install_config::ENSO_INSTALL_METADATA_PATH;
|
||||
use enso_install_config::INSTALLER_PAYLOAD_ID;
|
||||
use ide_ci::env::known::cargo::build::OUT_DIR;
|
||||
use ide_ci::programs::cargo;
|
||||
|
||||
|
||||
|
||||
fn main() {
|
||||
let rc_file = OUT_DIR.get().unwrap().join("archive.rc");
|
||||
cargo::build::rerun_if_file_changed(&rc_file);
|
||||
|
||||
cargo::build::rerun_if_env_changed(ENSO_INSTALL_ARCHIVE_PATH);
|
||||
if let Ok(archive) = ENSO_INSTALL_ARCHIVE_PATH.get() {
|
||||
cargo::build::rerun_if_file_changed(archive.as_str());
|
||||
// We need to either replace backslashes with forward slashes or escape them, as RC file is
|
||||
// kinda-compiled. The former is easier.
|
||||
let sanitized_path = archive.as_str().replace('\\', "/");
|
||||
let contents = format!(r#"{INSTALLER_PAYLOAD_ID} RCDATA "{sanitized_path}""#);
|
||||
ide_ci::fs::write_if_different(&rc_file, contents).unwrap();
|
||||
embed_resource::compile(&rc_file, embed_resource::NONE);
|
||||
} else {
|
||||
println!("cargo:warning={ENSO_INSTALL_ARCHIVE_PATH} is not set, the installer will fail at runtime.");
|
||||
}
|
||||
|
||||
cargo::build::rerun_if_env_changed(ENSO_INSTALL_METADATA_PATH);
|
||||
if !ENSO_INSTALL_METADATA_PATH.is_set() {
|
||||
println!("cargo:warning={ENSO_INSTALL_METADATA_PATH} is not set, the installer will fail at runtime.");
|
||||
let placeholder_path = OUT_DIR.get().unwrap().join("metadata.json");
|
||||
ide_ci::fs::write_if_different(&placeholder_path, "{}").unwrap();
|
||||
// Set env for the crate.
|
||||
cargo::build::expose_env_var(ENSO_INSTALL_METADATA_PATH, placeholder_path.as_str());
|
||||
}
|
||||
|
||||
let _ = sanitize_and_expose_electron_builder_config();
|
||||
}
|
56
build/install/installer/src/lib.rs
Normal file
56
build/install/installer/src/lib.rs
Normal file
@ -0,0 +1,56 @@
|
||||
//! This crate implements the Windows installer for the Enso IDE.
|
||||
|
||||
// === Features ===
|
||||
#![feature(lazy_cell)]
|
||||
|
||||
use enso_install::prelude::*;
|
||||
|
||||
use enso_install::access_built_time_env;
|
||||
|
||||
|
||||
// ==============
|
||||
// === Export ===
|
||||
// ==============
|
||||
|
||||
#[cfg(windows)]
|
||||
pub mod win;
|
||||
|
||||
pub use enso_install::prelude;
|
||||
|
||||
|
||||
|
||||
/// Update message sent by the installer logic thread to the UI.
|
||||
///
|
||||
/// These are used to communicate the progress of the installation process to the user.
|
||||
#[derive(Debug)]
|
||||
pub enum InstallerUpdate {
|
||||
/// Update the overall progress of the installation. The value is a number between 0 and 1.
|
||||
Progress(f64),
|
||||
/// Describe the current stage of the installation process.
|
||||
///
|
||||
/// The value will be displayed to the user, so it should be a human-readable string.
|
||||
Stage(String),
|
||||
/// The installation has finished.
|
||||
///
|
||||
/// If the Result is an error, the installation has failed and the error will be displayed to
|
||||
/// the user.
|
||||
Finished(Result),
|
||||
}
|
||||
|
||||
/// Handle to the compiled-in installer payload.
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub struct Payload {
|
||||
/// The binary data of the payload.
|
||||
pub data: &'static [u8],
|
||||
/// The metadata of the payload.
|
||||
pub metadata: &'static enso_install_config::payload::Metadata,
|
||||
}
|
||||
|
||||
/// Retrieve the compiled-in installer payload metadata.
|
||||
pub fn access_payload_metadata() -> &'static enso_install_config::payload::Metadata {
|
||||
access_built_time_env!(
|
||||
ENSO_INSTALL_METADATA_PATH,
|
||||
enso_install_config::payload::Metadata,
|
||||
"payload metadata"
|
||||
)
|
||||
}
|
23
build/install/installer/src/main.rs
Normal file
23
build/install/installer/src/main.rs
Normal file
@ -0,0 +1,23 @@
|
||||
#![windows_subsystem = "windows"] // Do not display a console window when running the installer.
|
||||
|
||||
use enso_installer::prelude::*;
|
||||
|
||||
|
||||
|
||||
#[cfg(windows)]
|
||||
fn main() -> Result {
|
||||
enso_installer::win::main()
|
||||
}
|
||||
|
||||
#[cfg(not(windows))]
|
||||
fn main() -> Result {
|
||||
bail!("This installer is only supported on Windows.")
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#[test]
|
||||
fn installer_name_matches() {
|
||||
assert_eq!(enso_install_config::INSTALLER_NAME, env!("CARGO_PKG_NAME"));
|
||||
}
|
||||
}
|
80
build/install/installer/src/win.rs
Normal file
80
build/install/installer/src/win.rs
Normal file
@ -0,0 +1,80 @@
|
||||
use ide_ci::prelude::*;
|
||||
|
||||
use crate::access_payload_metadata;
|
||||
use crate::win::config::Config;
|
||||
use crate::win::logic::install_with_updates;
|
||||
use crate::Payload;
|
||||
|
||||
use enso_install::win::local_app_data;
|
||||
|
||||
|
||||
// ==============
|
||||
// === Export ===
|
||||
// ==============
|
||||
|
||||
pub mod app;
|
||||
pub mod config;
|
||||
pub mod logic;
|
||||
|
||||
|
||||
|
||||
/// Retrieve the compiled-in installer payload with metadata.
|
||||
pub fn access_payload() -> Result<Payload> {
|
||||
Ok(Payload {
|
||||
data: enso_install::win::get_installer_payload()?,
|
||||
metadata: access_payload_metadata(),
|
||||
})
|
||||
}
|
||||
|
||||
/// Spawn a thread that will install the Enso IDE at the given location.
|
||||
pub fn spawn_installer_thread(
|
||||
install_location: impl AsRef<Path>,
|
||||
payload: Payload,
|
||||
config: Config,
|
||||
) -> Result<(std::thread::JoinHandle<Result>, std::sync::mpsc::Receiver<crate::InstallerUpdate>)> {
|
||||
let install_location = install_location.as_ref().to_path_buf();
|
||||
let (sender, receiver) = std::sync::mpsc::channel();
|
||||
let handle = std::thread::Builder::new()
|
||||
.name("Installer Logic".into())
|
||||
.spawn(move || {
|
||||
let result = install_with_updates(&install_location, payload, &config, &sender);
|
||||
if let Err(err) = result {
|
||||
let msg = format!("Installation failed: {err:?}.");
|
||||
let _ = sender.send(crate::InstallerUpdate::Finished(Result::Err(err)));
|
||||
bail!(msg);
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
.context("Failed to spawn the installer logic thread.")?;
|
||||
Ok((handle, receiver))
|
||||
}
|
||||
|
||||
|
||||
/// Get the default installation directory.
|
||||
pub fn get_install_dir(pretty_name: &str) -> Result<PathBuf> {
|
||||
let programs_dir = enso_install::win::user_program_files()
|
||||
.or_else(|e| {
|
||||
warn!("Failed to get the user's program files directory: {e:?}");
|
||||
// The Windows might refuse to provide the user's program files directory in some cases,
|
||||
// like brand-new user accounts that don't have the directory created yet.
|
||||
// Thus, we fall back to the default location, as documented in:
|
||||
// https://learn.microsoft.com/en-us/windows/win32/shell/knownfolderid
|
||||
Result::Ok(local_app_data()?.join("Programs"))
|
||||
})
|
||||
.context("Failed to get the user's program files directory")?;
|
||||
Ok(programs_dir.join(pretty_name))
|
||||
}
|
||||
|
||||
/// The installer's entry point.
|
||||
pub fn main() -> Result {
|
||||
// Note: logging will be set up by the `InstallerApp`. It needs to be responsible for this to be
|
||||
// able to show the log file to the user.
|
||||
let app = app::InstallerApp::new()?;
|
||||
app.run();
|
||||
if let Some(result) = app.result.take() {
|
||||
info!("Installation finished with result: {result:?}",);
|
||||
result
|
||||
} else {
|
||||
bail!("Installation finished without setting the result.")
|
||||
}
|
||||
}
|
233
build/install/installer/src/win/app.rs
Normal file
233
build/install/installer/src/win/app.rs
Normal file
@ -0,0 +1,233 @@
|
||||
use crate::prelude::*;
|
||||
|
||||
use native_windows_gui::NativeUi;
|
||||
use std::sync::mpsc::Receiver;
|
||||
|
||||
|
||||
|
||||
extern crate native_windows_gui as nwg;
|
||||
use crate::win::access_payload;
|
||||
use crate::win::config;
|
||||
use crate::win::get_install_dir;
|
||||
use crate::win::spawn_installer_thread;
|
||||
use crate::InstallerUpdate;
|
||||
|
||||
|
||||
pub mod ui;
|
||||
|
||||
/// The number of ticks the progress is divided into.
|
||||
pub const PROGRESS_BAR_TICKS: u32 = 1000;
|
||||
|
||||
/// The installer's UI application.
|
||||
///
|
||||
/// This struct holds all the UI elements and the logic to drive the installation process.
|
||||
/// The design follows the pattern recommended by the `native-windows-gui` library.
|
||||
// Note: we do not use the derive API, as:
|
||||
// - it does not support the `FlexboxLayout`,
|
||||
// - is generally more trouble than it's worth.
|
||||
#[derive(Default)]
|
||||
#[allow(missing_debug_implementations)]
|
||||
pub struct InstallerApp {
|
||||
/// Path to the log file. We use it to point the user to the log file in case of an error.
|
||||
pub logfile: PathBuf,
|
||||
/// Title used by the main window and some of the dialogs.
|
||||
pub window_title: String,
|
||||
/// The main window of the installer.
|
||||
pub window: nwg::Window,
|
||||
/// The main window's layout. Column with the top layout and the progress bar.
|
||||
pub layout: nwg::FlexboxLayout,
|
||||
/// Row with the Enso icon and the label.
|
||||
pub top_layout: nwg::FlexboxLayout,
|
||||
/// The Enso icon (displayed left to the label).
|
||||
pub image: nwg::ImageFrame,
|
||||
/// The label that shows the current stage of the installation.
|
||||
pub label: nwg::Label,
|
||||
/// The progress bar that shows the overall installation progress.
|
||||
pub progress_bar: nwg::ProgressBar,
|
||||
/// Handle to the embedded resources, such as the [`InstallerApp::enso_icon`].
|
||||
pub embed: nwg::EmbedResource,
|
||||
/// The icon handle that is displayed by the [`InstallerApp::image`].
|
||||
pub enso_icon: nwg::Icon,
|
||||
/// The timer we use to drive the [`InstallerApp::tick`] method.
|
||||
///
|
||||
/// Note that despite the name, the `AnimationTimer` is recommended to be used as a total
|
||||
/// replacement for the `Timer`.
|
||||
pub timer: nwg::AnimationTimer,
|
||||
/// Facilitates communication from the "installer backend" thread to the UI thread.
|
||||
pub backend_receiver: std::cell::RefCell<Option<Receiver<InstallerUpdate>>>,
|
||||
/// Handle to the thread that runs the installation logic.
|
||||
pub backend_thread: std::cell::RefCell<Option<std::thread::JoinHandle<Result>>>,
|
||||
/// Result of the installation process.
|
||||
///
|
||||
/// This should be filled by the UI thread before breaking the event loop.
|
||||
pub result: std::cell::RefCell<Option<Result>>,
|
||||
/// Path to the installed application executable.
|
||||
///
|
||||
/// After a successful installation, this is used to launch the application.
|
||||
pub installed_app: std::cell::RefCell<PathBuf>,
|
||||
}
|
||||
|
||||
impl InstallerApp {
|
||||
/// Create a new instance of the installer application.
|
||||
///
|
||||
/// This includes setting up logging.
|
||||
#[allow(clippy::new_ret_no_self)] // This follow the pattern advertised by the NWG crate.
|
||||
pub fn new() -> Result<ui::Ui> {
|
||||
let window_title =
|
||||
format!("{} installer", enso_install::sanitized_electron_builder_config().product_name);
|
||||
let dialog_title = window_title.clone();
|
||||
let logfile =
|
||||
enso_install::win::ui::setup_logging_or_fatal(env!("CARGO_PKG_NAME"), &window_title);
|
||||
let logfile_copy = logfile.clone();
|
||||
|
||||
// We intercept all the errors that can occur during the initialization of the UI.
|
||||
let result = move || -> Result<ui::Ui> {
|
||||
ui::init_global()?;
|
||||
let app = InstallerApp { logfile, window_title, ..Default::default() };
|
||||
InstallerApp::build_ui(app).context("Failed to build UI")
|
||||
}();
|
||||
if let Err(err) = &result {
|
||||
// We use "error" rather than "fatal", because "fatal" panics as soon as the error
|
||||
// dialog is closed. And we still want to open the logs.
|
||||
ui::error_message(&dialog_title, &format!("Installer failed to start: {err:?}"));
|
||||
let _ = ide_ci::programs::explorer::show_selected(logfile_copy);
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
/// Runs the installation.
|
||||
///
|
||||
/// # Panics
|
||||
/// In case of failure, shows an error message and opens the log file - then panics.
|
||||
pub fn run(&self) {
|
||||
let result = || -> Result {
|
||||
let config = config::fill_config()?;
|
||||
let install_dir = get_install_dir(&config.pretty_name)?;
|
||||
let payload = access_payload()?;
|
||||
let (handle, receiver) = spawn_installer_thread(&install_dir, payload, config.clone())?;
|
||||
self.backend_receiver.borrow_mut().replace(receiver);
|
||||
self.backend_thread.borrow_mut().replace(handle);
|
||||
*self.installed_app.borrow_mut() = install_dir.join(&config.executable_filename);
|
||||
Ok(())
|
||||
}()
|
||||
.context("Failed to start installation.");
|
||||
if let Err(err) = result {
|
||||
self.fail_installation(err);
|
||||
} else {
|
||||
debug!("Starting event loop");
|
||||
nwg::dispatch_thread_events();
|
||||
debug!("Event loop finished");
|
||||
}
|
||||
}
|
||||
|
||||
/// Handle a single event dispatched by the event loop.
|
||||
pub fn handle_ui_event(
|
||||
&self,
|
||||
event: nwg::Event,
|
||||
evt_data: nwg::EventData,
|
||||
handle: nwg::ControlHandle,
|
||||
) {
|
||||
match event {
|
||||
nwg::Event::OnTimerTick =>
|
||||
if handle == self.timer {
|
||||
self.tick();
|
||||
},
|
||||
nwg::Event::OnWindowClose => {
|
||||
// Prevent manual closing of the window. Installation should not be interrupted.
|
||||
if let nwg::EventData::OnWindowClose(close_data) = evt_data {
|
||||
close_data.close(false);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Stop the event loop due to an installation error.
|
||||
///
|
||||
/// Shows the dialog with the error message and points the user to the log file.
|
||||
pub fn fail_installation(&self, error: anyhow::Error) {
|
||||
let msg = format!("{error:?}");
|
||||
let _ = self.result.borrow_mut().replace(Result::Err(error));
|
||||
self.label.set_text("Installation failed.");
|
||||
self.progress_bar.set_state(nwg::ProgressBarState::Error);
|
||||
self.progress_bar.set_pos(100);
|
||||
|
||||
error!(msg);
|
||||
info!("Showing modal error message.");
|
||||
nwg::modal_error_message(&self.window, &self.window_title, &msg);
|
||||
info!("Stopping the event loop due to an error.");
|
||||
nwg::stop_thread_dispatch();
|
||||
info!("Showing the log file in the file explorer.");
|
||||
let _ = ide_ci::programs::explorer::show_selected(&self.logfile);
|
||||
}
|
||||
|
||||
/// Mark installation as successful and stop the event loop.
|
||||
pub fn succeed_installation(&self) {
|
||||
info!("Installation completed successfully.");
|
||||
let _ = self.result.borrow_mut().replace(Ok(()));
|
||||
self.label.set_text("Installation completed successfully.");
|
||||
nwg::stop_thread_dispatch();
|
||||
self.window.close();
|
||||
|
||||
let installed_app = &self.installed_app.borrow();
|
||||
info!("Starting the installed application: {}", installed_app.display());
|
||||
let _ = Command::new(&**installed_app).spawn().inspect_err(|err| {
|
||||
// We won't stop the whole world if we can't start the installed application.
|
||||
// Still, we should leave some trace of what happened.
|
||||
error!("Failed to start the installed application: {err:?}");
|
||||
});
|
||||
}
|
||||
|
||||
/// Method called by the timer to update the UI.
|
||||
///
|
||||
/// It pulls updates from the installer backend and updates the UI accordingly.
|
||||
pub fn tick(&self) {
|
||||
let Ok(installer_state) = self.backend_receiver.try_borrow_mut() else {
|
||||
// If the receiver is already borrowed, it means that we have re-entered this method
|
||||
// from the failure dialog event loop. In such case we do nothing.
|
||||
return;
|
||||
};
|
||||
|
||||
if let Some(receiver) = installer_state.deref() {
|
||||
loop {
|
||||
if self.result.borrow().is_some() {
|
||||
// If the installation has already finished, we don't need to do anything.
|
||||
return;
|
||||
}
|
||||
|
||||
match receiver.try_recv() {
|
||||
Ok(update) => {
|
||||
info!("Update: {:?}", update);
|
||||
match update {
|
||||
InstallerUpdate::Progress(progress) => {
|
||||
let new_ticks = (progress * PROGRESS_BAR_TICKS as f64) as u32;
|
||||
self.progress_bar.set_pos(new_ticks);
|
||||
}
|
||||
InstallerUpdate::Stage(stage) => {
|
||||
self.label.set_text(&stage);
|
||||
}
|
||||
InstallerUpdate::Finished(result) => {
|
||||
if let Err(err) = result {
|
||||
self.fail_installation(err);
|
||||
} else {
|
||||
self.succeed_installation();
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(std::sync::mpsc::TryRecvError::Empty) => break,
|
||||
Err(std::sync::mpsc::TryRecvError::Disconnected) => {
|
||||
// We expect to receive `InstallerUpdate::Finished` before the channel is
|
||||
// closed. Otherwise, it means that the backend thread has crashed.
|
||||
let err =
|
||||
anyhow!("The installer backend thread has unexpectedly disconnected.");
|
||||
self.fail_installation(err);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
151
build/install/installer/src/win/app/ui.rs
Normal file
151
build/install/installer/src/win/app/ui.rs
Normal file
@ -0,0 +1,151 @@
|
||||
//! The UI setup for the installer application.
|
||||
//!
|
||||
//! This is roughly what `native-windows-derive` would generate.
|
||||
|
||||
use crate::prelude::*;
|
||||
|
||||
use std::cell::RefCell;
|
||||
use std::rc::Rc;
|
||||
|
||||
|
||||
|
||||
extern crate native_windows_gui as nwg;
|
||||
|
||||
use crate::win::app::InstallerApp;
|
||||
use crate::win::app::PROGRESS_BAR_TICKS;
|
||||
use enso_install_config::ENSO_ICON_ID;
|
||||
use nwg::stretch::geometry::Size;
|
||||
use nwg::stretch::style::Dimension;
|
||||
use nwg::stretch::style::FlexDirection;
|
||||
use nwg::NativeUi;
|
||||
|
||||
|
||||
/// Size for the Enso icon displayed in the window next to the text label.
|
||||
pub const ICON_SIZE: u32 = 32;
|
||||
|
||||
/// Default font used in the application.
|
||||
///
|
||||
/// Segoe UI is the default font in Windows since Vista and we can reasonably expect it to be
|
||||
/// available.
|
||||
pub const DEFAULT_FONT: &str = "Segoe UI";
|
||||
|
||||
pub struct Ui {
|
||||
/// Inner application data, that is shared with the event callbacks.
|
||||
inner: Rc<InstallerApp>,
|
||||
/// Main events handler handle - so we can unbind it when the UI is dropped.
|
||||
default_handler: RefCell<Option<nwg::EventHandler>>,
|
||||
}
|
||||
|
||||
impl Debug for Ui {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("Ui").finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl NativeUi<Ui> for InstallerApp {
|
||||
fn build_ui(mut data: InstallerApp) -> std::result::Result<Ui, nwg::NwgError> {
|
||||
nwg::EmbedResource::builder().build(&mut data.embed)?;
|
||||
nwg::Icon::builder()
|
||||
.source_embed(Some(&data.embed))
|
||||
.source_embed_str(Some(ENSO_ICON_ID))
|
||||
.strict(false)
|
||||
.size(Some((ICON_SIZE, ICON_SIZE)))
|
||||
.build(&mut data.enso_icon)?;
|
||||
nwg::Window::builder()
|
||||
.title(&data.window_title)
|
||||
.flags(nwg::WindowFlags::WINDOW | nwg::WindowFlags::VISIBLE)
|
||||
.icon(Some(&data.enso_icon))
|
||||
.size((400, 100))
|
||||
.build(&mut data.window)?;
|
||||
nwg::ImageFrame::builder()
|
||||
.icon(Some(&data.enso_icon))
|
||||
.size((ICON_SIZE as i32, ICON_SIZE as i32))
|
||||
.parent(&data.window)
|
||||
.build(&mut data.image)?;
|
||||
nwg::Label::builder()
|
||||
.text("Preparing the installer...")
|
||||
.parent(&data.window)
|
||||
.build(&mut data.label)?;
|
||||
nwg::ProgressBar::builder()
|
||||
.step(1)
|
||||
.range(0..PROGRESS_BAR_TICKS)
|
||||
.parent(&data.window)
|
||||
.build(&mut data.progress_bar)?;
|
||||
nwg::AnimationTimer::builder()
|
||||
.parent(&data.window)
|
||||
.interval(std::time::Duration::from_millis(100))
|
||||
.active(true)
|
||||
.build(&mut data.timer)?;
|
||||
let inner = Rc::new(data);
|
||||
let ui = Ui { inner: inner.clone(), default_handler: Default::default() };
|
||||
|
||||
let evt_ui = Rc::downgrade(&inner);
|
||||
let handle_events = move |evt, evt_data, handle| {
|
||||
if let Some(evt_ui) = evt_ui.upgrade() {
|
||||
evt_ui.handle_ui_event(evt, evt_data, handle);
|
||||
}
|
||||
};
|
||||
|
||||
let event_handler = nwg::full_bind_event_handler(&ui.window.handle, handle_events);
|
||||
*ui.default_handler.borrow_mut() = Some(event_handler);
|
||||
|
||||
nwg::FlexboxLayout::builder()
|
||||
.parent(&ui.window)
|
||||
.flex_direction(FlexDirection::Row)
|
||||
.child(&ui.image)
|
||||
.child_size(Size {
|
||||
width: Dimension::Points(ICON_SIZE as f32),
|
||||
height: Dimension::Points(ICON_SIZE as f32),
|
||||
})
|
||||
.child(&ui.label)
|
||||
.child_flex_grow(1.0)
|
||||
.child_size(Size { width: Dimension::Auto, height: Dimension::Auto })
|
||||
.build_partial(&ui.top_layout)?;
|
||||
nwg::FlexboxLayout::builder()
|
||||
.parent(&ui.window)
|
||||
.flex_direction(FlexDirection::Column)
|
||||
.child_layout(&ui.top_layout)
|
||||
.child_size(Size { width: Dimension::Percent(1.0), height: Dimension::Auto })
|
||||
.child(&ui.progress_bar)
|
||||
.child_size(Size { width: Dimension::Percent(0.9), height: Dimension::Points(16.0) })
|
||||
.build(&ui.layout)?;
|
||||
Ok(ui)
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Ui {
|
||||
/// To make sure that everything is freed without issues, the default handler must be
|
||||
/// unbound.
|
||||
fn drop(&mut self) {
|
||||
let handler = self.default_handler.borrow();
|
||||
if let Some(handler) = handler.as_ref() {
|
||||
nwg::unbind_event_handler(handler);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for Ui {
|
||||
type Target = InstallerApp;
|
||||
fn deref(&self) -> &InstallerApp {
|
||||
&self.inner
|
||||
}
|
||||
}
|
||||
|
||||
/// Display an error message to the user and log it.
|
||||
pub fn error_message(title: &str, message: &str) {
|
||||
error!("{message}");
|
||||
nwg::error_message(title, message);
|
||||
}
|
||||
|
||||
/// Init the global state of the `native-windows-gui` library.
|
||||
pub fn init_global() -> Result {
|
||||
nwg::init().context("Failed to init Native Windows GUI")?;
|
||||
let mut font = nwg::Font::default();
|
||||
nwg::FontBuilder::new()
|
||||
.family(DEFAULT_FONT)
|
||||
.size(16)
|
||||
.build(&mut font)
|
||||
.context("Failed to create default font")?;
|
||||
nwg::Font::set_global_default(Some(font));
|
||||
Ok(())
|
||||
}
|
88
build/install/installer/src/win/config.rs
Normal file
88
build/install/installer/src/win/config.rs
Normal file
@ -0,0 +1,88 @@
|
||||
//! Information defining the installer's behavior.
|
||||
|
||||
use crate::prelude::*;
|
||||
|
||||
|
||||
|
||||
/// All the configuration and constants needed to build the installer.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Config {
|
||||
/// E.g. `Enso.exe`.
|
||||
pub executable_filename: PathBuf,
|
||||
|
||||
/// E.g. `New Byte Order sp. z o.o.`.
|
||||
pub publisher: String,
|
||||
|
||||
/// E.g. `Enso`.
|
||||
pub pretty_name: String,
|
||||
|
||||
/// E.g. `Enso`.
|
||||
///
|
||||
/// Used for entries in the Start Menu and Desktop.
|
||||
pub shortcut_name: String,
|
||||
|
||||
/// The name of the registry key where uninstall information is stored, e.g. `Enso`.
|
||||
///
|
||||
/// The key is located under
|
||||
/// `HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Uninstall`.
|
||||
pub uninstall_key: String,
|
||||
|
||||
/// Version of the application.
|
||||
pub version: Version,
|
||||
|
||||
/// The URL protocols that will be registered for the application, e.g. `enso`.
|
||||
pub url_protocols: Vec<String>,
|
||||
|
||||
/// File associations.
|
||||
pub file_associations:
|
||||
Vec<(enso_install::win::prog_id::FileType, enso_install::win::prog_id::FileExtension)>,
|
||||
}
|
||||
|
||||
/// Generate the `Config` for the installer from the compiled-in Electron Builder configuration.
|
||||
pub fn fill_config() -> Result<Config> {
|
||||
let electron = enso_install::sanitized_electron_builder_config();
|
||||
|
||||
let executable_filename = electron.product_name.with_executable_extension();
|
||||
let publisher = electron.extra_metadata.installer.publisher.clone();
|
||||
let pretty_name = electron.product_name.clone();
|
||||
let shortcut_name = pretty_name.clone();
|
||||
let uninstall_key = pretty_name.clone();
|
||||
let version = electron.extra_metadata.version.clone();
|
||||
let url_protocols = electron.protocols.iter().flat_map(|p| &p.schemes).map_into().collect();
|
||||
let file_associations = electron
|
||||
.extra_metadata
|
||||
.installer
|
||||
.file_associations
|
||||
.iter()
|
||||
.map(|file_association| {
|
||||
let prog_id = file_association.prog_id.clone();
|
||||
let extension = file_association.ext.clone();
|
||||
let mime_type = file_association.mime_type.clone();
|
||||
let perceived_type =
|
||||
enso_install::win::prog_id::PerceivedType::from_mime_type(&mime_type)?;
|
||||
let file_type = enso_install::win::prog_id::FileType {
|
||||
application_path: executable_filename.clone(),
|
||||
prog_id: prog_id.clone(),
|
||||
friendly_name: file_association.name.clone(),
|
||||
info_tip: file_association.name.clone(),
|
||||
};
|
||||
let file_extension = enso_install::win::prog_id::FileExtension {
|
||||
extension,
|
||||
prog_id,
|
||||
mime_type,
|
||||
perceived_type,
|
||||
};
|
||||
Result::Ok((file_type, file_extension))
|
||||
})
|
||||
.try_collect()?;
|
||||
Ok(Config {
|
||||
executable_filename,
|
||||
publisher,
|
||||
pretty_name,
|
||||
shortcut_name,
|
||||
uninstall_key,
|
||||
version,
|
||||
url_protocols,
|
||||
file_associations,
|
||||
})
|
||||
}
|
264
build/install/installer/src/win/logic.rs
Normal file
264
build/install/installer/src/win/logic.rs
Normal file
@ -0,0 +1,264 @@
|
||||
//! Code that performs the installation.
|
||||
|
||||
use crate::prelude::*;
|
||||
|
||||
use crate::win::config::Config;
|
||||
use crate::Payload;
|
||||
|
||||
use enso_install_config::UNINSTALLER_NAME;
|
||||
use flate2::read::GzDecoder;
|
||||
|
||||
|
||||
|
||||
/// Register file extensions and their associations in the Windows registry.
|
||||
pub fn register_file_associations(
|
||||
file_associations: &[(
|
||||
enso_install::win::prog_id::FileType,
|
||||
enso_install::win::prog_id::FileExtension,
|
||||
)],
|
||||
) -> Result {
|
||||
for (file_type, file_extension) in file_associations {
|
||||
info!("Registering file extension '{}'.", file_extension.extension);
|
||||
file_extension.register()?;
|
||||
|
||||
info!("Registering file type '{}'.", file_type.prog_id);
|
||||
file_type.register()?;
|
||||
}
|
||||
|
||||
info!("Refreshing file associations in the shell.");
|
||||
enso_install::win::refresh_file_associations();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Set application as the default handler for the given URL protocol, e.g. `enso://`.
|
||||
///
|
||||
/// This is necessary for the deep linking to work.
|
||||
pub fn register_url_protocol(executable_path: &Path, protocol: &str) -> Result {
|
||||
let info = enso_install::win::prog_id::ProtocolInfo::new(protocol, executable_path);
|
||||
info.register()
|
||||
}
|
||||
|
||||
/// Register the uninstaller in the Windows registry.
|
||||
pub fn register_uninstaller(
|
||||
config: &Config,
|
||||
install_directory: &Path,
|
||||
uninstaller_path: &Path,
|
||||
installation_size_bytes: u64,
|
||||
) -> Result {
|
||||
let mut uninstall_info = enso_install::win::uninstall::UninstallInfo::new(
|
||||
&config.pretty_name,
|
||||
uninstaller_path.as_str(),
|
||||
);
|
||||
uninstall_info.install_location = Some(install_directory.display().to_string());
|
||||
uninstall_info.install_date = Some(chrono::Local::now().to_string());
|
||||
uninstall_info.publisher = Some(config.publisher.clone());
|
||||
uninstall_info.display_icon = Some(uninstaller_path.display().to_string());
|
||||
uninstall_info.display_version = Some(config.version.to_string());
|
||||
uninstall_info.estimated_size_kib = Some((installation_size_bytes / 1024) as u32);
|
||||
uninstall_info.write_to_registry(&config.uninstall_key)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Install Enso.
|
||||
///
|
||||
/// The archive payload is binary data of the tar.gz archive that contains the Enso app.
|
||||
pub fn install_with_updates(
|
||||
install_location: &Path,
|
||||
payload: Payload,
|
||||
config: &Config,
|
||||
sender: &std::sync::mpsc::Sender<crate::InstallerUpdate>,
|
||||
) -> Result {
|
||||
let send = |update| {
|
||||
info!("Sending update: {update:?}");
|
||||
let _ = sender.send(update);
|
||||
};
|
||||
let report_progress = |progress| {
|
||||
send(crate::InstallerUpdate::Progress(progress));
|
||||
};
|
||||
macro_rules! stage_at {
|
||||
($progress:tt, $($arg:tt)*) => {
|
||||
send(crate::InstallerUpdate::Stage(format!($($arg)*)));
|
||||
send(crate::InstallerUpdate::Progress($progress));
|
||||
};
|
||||
}
|
||||
macro_rules! bail {
|
||||
($($arg:tt)*) => {{
|
||||
let msg = format!($($arg)*);
|
||||
let err = anyhow::Error::msg(msg.clone());
|
||||
send(crate::InstallerUpdate::Finished(Err(err)));
|
||||
anyhow::bail!("{msg}");
|
||||
}};
|
||||
}
|
||||
|
||||
// Only one installer / uninstaller can run at a time.
|
||||
let _guard = enso_install::locked_installation_lock()?;
|
||||
|
||||
let enso_install_config::payload::Metadata { total_files, total_bytes } = *payload.metadata;
|
||||
stage_at!(0.00, "Checking disk space.");
|
||||
// TODO? A potential improvement would be to take account for previous installation size when
|
||||
// performing the in-place update. Then the needed space would be the difference between
|
||||
// the new and the old installation size.
|
||||
let per_file_overhead = 4096; // The default allocation unit size on NTFS.
|
||||
let space_required = total_bytes + (total_files * per_file_overhead);
|
||||
match check_disk_space(install_location, space_required) {
|
||||
Ok(Some(msg)) => bail!("{msg}"),
|
||||
Ok(None) => {} // Ok, enough space.
|
||||
Err(err) => {
|
||||
// We don't know, so let's just log the warning and try to carry on.
|
||||
warn!("Failed to check disk space: {err:?}");
|
||||
}
|
||||
}
|
||||
|
||||
stage_at!(0.01, "Checking for running processes.");
|
||||
match enso_install::is_already_running(install_location, &[]) {
|
||||
Ok(Some(msg)) => bail!("{msg}"),
|
||||
Ok(None) => {} // Ok, no colliding processes.
|
||||
Err(err) => {
|
||||
// We don't know, so let's just log the warning and try to carry on.
|
||||
warn!("Failed to check for running processes: {err:?}");
|
||||
}
|
||||
}
|
||||
|
||||
stage_at!(0.03, "Removing old installation files (if present).");
|
||||
ide_ci::fs::reset_dir(install_location)?;
|
||||
|
||||
let executable_location = install_location.join(&config.executable_filename);
|
||||
|
||||
// Extract the files.
|
||||
let decoder = GzDecoder::new(payload.data);
|
||||
let archive = tar::Archive::new(decoder);
|
||||
|
||||
let extraction_progress_start = 0.06;
|
||||
let extraction_progress_step = 0.82;
|
||||
let mut files_extracted = 0;
|
||||
let mut bytes_extracted = 0;
|
||||
|
||||
stage_at!(extraction_progress_start, "Extracting files.");
|
||||
let mut bytes_being_extracted = 0;
|
||||
let to_our_path = |entry: &tar::Entry<GzDecoder<&[u8]>>| -> Option<PathBuf> {
|
||||
// If we receive a new file, update the counters.
|
||||
files_extracted += 1;
|
||||
bytes_extracted += bytes_being_extracted;
|
||||
bytes_being_extracted = entry.header().size().unwrap_or(0);
|
||||
|
||||
let files_ratio = (files_extracted as f64 / total_files as f64).min(1.0);
|
||||
let bytes_ratio = (bytes_extracted as f64 / total_bytes as f64).min(1.0);
|
||||
let extraction_progresss = (files_ratio + bytes_ratio) / 2.0;
|
||||
let progress = extraction_progress_start + extraction_progress_step * extraction_progresss;
|
||||
trace!("files_extracted: {files_extracted}/{total_files}, bytes_extracted: {bytes_extracted}/{total_bytes}, extraction_progresss: {extraction_progresss}, progress: {progress}");
|
||||
report_progress(progress);
|
||||
Some(install_location.join(entry.path().ok()?))
|
||||
};
|
||||
|
||||
ide_ci::archive::tar::extract_files_sync(archive, to_our_path)?;
|
||||
// As we've been incrementing this values when extracting the next file, we need to cover the
|
||||
// last file.
|
||||
bytes_extracted += bytes_being_extracted;
|
||||
|
||||
|
||||
let post_extraction_progress = extraction_progress_start + extraction_progress_step;
|
||||
|
||||
stage_at!(post_extraction_progress, "Registering file types.");
|
||||
register_file_associations(&config.file_associations)?;
|
||||
|
||||
for protocol in &config.url_protocols {
|
||||
stage_at!(0.90, "Registering URL protocol '{protocol}'.");
|
||||
register_url_protocol(&executable_location, protocol)?;
|
||||
}
|
||||
|
||||
stage_at!(0.92, "Registering the application path.");
|
||||
let app_paths_info = enso_install::win::app_paths::AppPathInfo::new(&executable_location);
|
||||
app_paths_info.write_to_registry()?;
|
||||
|
||||
stage_at!(0.94, "Registering the uninstaller.");
|
||||
register_uninstaller(
|
||||
config,
|
||||
install_location,
|
||||
&install_location.join(UNINSTALLER_NAME.with_executable_extension()),
|
||||
bytes_extracted,
|
||||
)?;
|
||||
|
||||
stage_at!(0.96, "Creating Start Menu entry.");
|
||||
enso_install::win::shortcut::Location::Menu
|
||||
.create_shortcut(&config.shortcut_name, &executable_location)?;
|
||||
|
||||
stage_at!(0.98, "Creating Desktop shortcut.");
|
||||
enso_install::win::shortcut::Location::Desktop
|
||||
.create_shortcut(&config.shortcut_name, &executable_location)?;
|
||||
|
||||
|
||||
stage_at!(1.0, "Installation complete.");
|
||||
send(crate::InstallerUpdate::Finished(Ok(())));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Check if there is enough disk space to install the application.
|
||||
///
|
||||
/// If the space is insufficient, returns an error message. If the space is sufficient, returns
|
||||
/// `None`. If the necessary information cannot be obtained, returns an error.
|
||||
///
|
||||
/// Note that usually it is better to ignore the error than to fail the installation process. Not
|
||||
/// knowing that the disk space is sufficient is not meaning that it is insufficient.
|
||||
/// For example, we might be targetting a network path for which we cannot obtain the disk space.
|
||||
pub fn check_disk_space(
|
||||
installation_directory: &Path,
|
||||
bytes_required: u64,
|
||||
) -> Result<Option<String>> {
|
||||
use sysinfo::Disks;
|
||||
let disks = Disks::new_with_refreshed_list();
|
||||
// This should yield an absolute path, prefixed with the drive label.
|
||||
let path = installation_directory
|
||||
.canonicalize()
|
||||
// We use absolutize as a fallback, because canonicalize fails for non-existent paths. We
|
||||
// attempt to use canonicalize first, because it resolves symlinks.
|
||||
.or_else(|_| installation_directory.absolutize().map(PathBuf::from))?;
|
||||
|
||||
// We need to remove the verbatim prefix (that canonicalize likes to add) in order to match the
|
||||
// disk list mount points format.
|
||||
let path = path.without_verbatim_prefix();
|
||||
let disk = disks
|
||||
.into_iter()
|
||||
.find(|disk| path.starts_with(disk.mount_point()))
|
||||
.context("No disk information found for the installation directory.")?;
|
||||
|
||||
let required_space = byte_unit::Byte::from_u64(bytes_required);
|
||||
let free_space = byte_unit::Byte::from_u64(disk.available_space());
|
||||
|
||||
if free_space < required_space {
|
||||
let msg = format!(
|
||||
"Not enough disk space on {} to install. Required: {:.2}, available: {:.2}.",
|
||||
disk.mount_point().display(),
|
||||
required_space.get_appropriate_unit(byte_unit::UnitType::Binary),
|
||||
free_space.get_appropriate_unit(byte_unit::UnitType::Binary)
|
||||
);
|
||||
return Ok(Some(msg));
|
||||
}
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
#[ignore]
|
||||
/// Test to manually check the generated disk space message.
|
||||
fn check_disk_space_test() -> Result {
|
||||
let my_path = ide_ci::env::current_dir()?;
|
||||
let r = check_disk_space(&my_path, 10_000_000_000_000);
|
||||
let _ = dbg!(r);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore]
|
||||
/// Test to manually check the running processes.
|
||||
fn is_already_running_test() -> Result {
|
||||
setup_logging().ok();
|
||||
let install_path = crate::win::get_install_dir("Enso")?;
|
||||
let r = enso_install::is_already_running(&install_path, &[])?;
|
||||
dbg!(r);
|
||||
Ok(())
|
||||
}
|
||||
}
|
176
build/install/src/lib.rs
Normal file
176
build/install/src/lib.rs
Normal file
@ -0,0 +1,176 @@
|
||||
//! This crate is linked in both by the installer and the uninstaller.
|
||||
|
||||
// === Features ===
|
||||
#![feature(lazy_cell)]
|
||||
|
||||
|
||||
|
||||
pub mod prelude {
|
||||
pub use ide_ci::prelude::*;
|
||||
|
||||
#[cfg(windows)]
|
||||
pub use winreg::types::ToRegValue;
|
||||
#[cfg(windows)]
|
||||
pub use winreg::RegKey;
|
||||
#[cfg(windows)]
|
||||
pub use winreg::RegValue;
|
||||
}
|
||||
|
||||
use enso_install_config::electron_builder;
|
||||
use ide_ci::log::file_log_layer;
|
||||
use ide_ci::log::stderr_log_layer;
|
||||
use ide_ci::log::GlobalFilteringLayer;
|
||||
use prelude::*;
|
||||
use sysinfo::Pid;
|
||||
|
||||
#[cfg(windows)]
|
||||
pub mod win;
|
||||
|
||||
|
||||
/// A macro for accessing compiled-in JSON data.
|
||||
///
|
||||
/// # Parameters
|
||||
/// - `$env` - the name of the environment variable that contains the path to the JSON file.
|
||||
/// - `$typename` - the type of the data that is stored in the JSON file.
|
||||
/// - `$pretty_name` - a human-readable name of the data type that will be used in error messages.
|
||||
#[macro_export]
|
||||
macro_rules! access_built_time_env {
|
||||
($env:ident, $typename:ty, $pretty_name:expr) => {
|
||||
{
|
||||
static DATA: std::sync::LazyLock<$typename> = std::sync::LazyLock::new(|| {
|
||||
let crate_name = env!("CARGO_PKG_NAME");
|
||||
let pretty = $pretty_name;
|
||||
let path = env!(stringify!($env));
|
||||
let data = include_str!(env!(stringify!($env)));
|
||||
if path.is_empty() {
|
||||
panic!("The path to the {pretty} is empty. The {crate_name} was built without `{}` environment variable set.", stringify!($env));
|
||||
}
|
||||
if data.is_empty() {
|
||||
panic!("The {pretty} file is empty. Likely the stub was provided to enable compilation. Investigate the build logs warnings.");
|
||||
}
|
||||
serde_json::from_str(data).expect(&format!("Failed to parse the {pretty}."))
|
||||
});
|
||||
&DATA
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/// Access compiled-in `electron builder`-based configuration.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// This function will panic if the path to the configuration was not set during the build process,
|
||||
/// or if the configuration was invalid.
|
||||
pub fn sanitized_electron_builder_config() -> &'static electron_builder::Config {
|
||||
access_built_time_env!(
|
||||
ENSO_INSTALL_ELECTRON_BUILDER_CONFIG,
|
||||
electron_builder::Config,
|
||||
"Electron Builder configuration"
|
||||
)
|
||||
}
|
||||
|
||||
/// The name of the Windows registry key where uninstall information is stored.
|
||||
///
|
||||
/// The key is located under
|
||||
/// `HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Uninstall`.
|
||||
pub fn uninstall_key() -> &'static str {
|
||||
&sanitized_electron_builder_config().product_name
|
||||
}
|
||||
|
||||
/// The full filename (not path!) of the application executable, including the extension.
|
||||
pub fn executable_filename() -> PathBuf {
|
||||
sanitized_electron_builder_config().product_name.with_executable_extension()
|
||||
}
|
||||
|
||||
/// The name of the shortcut.
|
||||
///
|
||||
/// Used on Windows for Start Menu and Desktop shortcuts.
|
||||
pub fn shortcut_name() -> &'static str {
|
||||
&sanitized_electron_builder_config().product_name
|
||||
}
|
||||
|
||||
/// Acquire a named file lock.
|
||||
///
|
||||
/// The lock is to be used to ensure that only one instance of the (un)installer is running at a
|
||||
/// time.
|
||||
pub fn installation_file_lock() -> Result<named_lock::NamedLock> {
|
||||
let name = env!("CARGO_PKG_NAME");
|
||||
let lock = named_lock::NamedLock::create(name)
|
||||
.with_context(|| format!("Failed to create a named file lock for '{name}'."))?;
|
||||
Ok(lock)
|
||||
}
|
||||
|
||||
/// Acquire the named file lock and return the guard.
|
||||
pub fn locked_installation_lock() -> Result<named_lock::NamedLockGuard> {
|
||||
installation_file_lock()?.try_lock().with_context(|| "Failed to acquire the named file lock. Is there another instance of the installer or uninstaller running?")
|
||||
}
|
||||
|
||||
/// Check if the application is already running.
|
||||
///
|
||||
/// If there is any process running from the installation directory, returns an error message as
|
||||
/// `Some`. If there are no such processes, returns `None`. If the necessary information cannot be
|
||||
/// obtained, returns an error.
|
||||
///
|
||||
/// The processes are matched using their executable paths. Processes for which the path cannot be
|
||||
/// obtained are ignored.
|
||||
pub fn is_already_running(install_dir: &Path, ignored_pids: &[Pid]) -> Result<Option<String>> {
|
||||
let install_dir = install_dir.canonicalize()?;
|
||||
let mut offending_processes = vec![];
|
||||
|
||||
// First get process list.
|
||||
let mut sys = sysinfo::System::new();
|
||||
sys.refresh_processes();
|
||||
for (pid, process) in sys.processes() {
|
||||
if ignored_pids.contains(pid) {
|
||||
trace!("Process {} ({}) is ignored.", process.name(), pid);
|
||||
continue;
|
||||
}
|
||||
let Some(path) = process.exe() else {
|
||||
warn!("Process {} ({}) has no path.", process.name(), pid);
|
||||
continue;
|
||||
};
|
||||
let Ok(path) = path.canonicalize() else {
|
||||
warn!("Failed to canonicalize process path: {}", path.display());
|
||||
continue;
|
||||
};
|
||||
|
||||
if path.starts_with(&install_dir) {
|
||||
offending_processes.push(process);
|
||||
info!("Process {} ({}) is in the installation directory.", process.name(), pid);
|
||||
} else {
|
||||
trace!("Process {} ({}) is not in the installation directory.", process.name(), pid);
|
||||
}
|
||||
}
|
||||
|
||||
if !offending_processes.is_empty() {
|
||||
let processes_list = offending_processes
|
||||
.iter()
|
||||
.map(|p| format!(" * {} (pid {})", p.name(), p.pid()))
|
||||
.join("\n");
|
||||
let message = format!("It seems that the application is currently running. Please close it before running the installer.\n\nThe following processes are running from the installation directory:\n{}", processes_list);
|
||||
Ok(Some(message))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
/// Setup logging for the installer. It logs both to stderr and to a file in the temp directory.
|
||||
///
|
||||
/// Returns the path to the generated log file.
|
||||
pub fn setup_logging(app_name: &str) -> Result<PathBuf> {
|
||||
use tracing_subscriber::prelude::*;
|
||||
// Generate filename based on the current time.
|
||||
let timestamp = chrono::Local::now().format("%Y-%m-%d %H-%M-%S");
|
||||
let filename = format!("{app_name}-{timestamp}.log");
|
||||
let temp_location = std::env::temp_dir();
|
||||
let log_file = temp_location.join(filename);
|
||||
let file = ide_ci::fs::create(&log_file)?;
|
||||
let registry = tracing_subscriber::Registry::default()
|
||||
.with(GlobalFilteringLayer)
|
||||
.with(stderr_log_layer())
|
||||
.with(file_log_layer(file));
|
||||
|
||||
tracing::subscriber::set_global_default(registry)
|
||||
.context("Failed to set global default subscriber.")?;
|
||||
Ok(log_file)
|
||||
}
|
192
build/install/src/win.rs
Normal file
192
build/install/src/win.rs
Normal file
@ -0,0 +1,192 @@
|
||||
//! Windows-specific code for the Enso installer.
|
||||
|
||||
// === Non-Standard Linter Configuration ===
|
||||
#![allow(unsafe_code)]
|
||||
|
||||
use crate::prelude::*;
|
||||
use winreg::enums::*;
|
||||
|
||||
use enso_install_config::ENSO_INSTALL_ARCHIVE_PATH;
|
||||
use enso_install_config::INSTALLER_PAYLOAD_ID;
|
||||
use std::ffi::c_void;
|
||||
#[cfg(windows)]
|
||||
use std::os::windows::ffi::OsStringExt;
|
||||
use windows::core::PCWSTR;
|
||||
use windows::Win32::UI::Shell;
|
||||
|
||||
|
||||
// ==============
|
||||
// === Export ===
|
||||
// ==============
|
||||
|
||||
pub mod app_paths;
|
||||
pub mod prog_id;
|
||||
pub mod registry;
|
||||
pub mod resource;
|
||||
pub mod shortcut;
|
||||
pub mod ui;
|
||||
pub mod uninstall;
|
||||
|
||||
|
||||
|
||||
/// Open the `HKEY_CURRENT_USER\Software\Classes` key for reading and writing.
|
||||
///
|
||||
/// This is where the programmatic identifiers (ProgIDs) of file types and URL protocols are
|
||||
/// stored.
|
||||
pub fn classes_key() -> Result<RegKey> {
|
||||
RegKey::predef(HKEY_CURRENT_USER)
|
||||
.open_subkey_with_flags(r"Software\Classes", KEY_READ | KEY_WRITE)
|
||||
.context(r"Failed to open `HKEY_CURRENT_USER\Software\Classes` key.")
|
||||
}
|
||||
|
||||
|
||||
/// Get the local user's Desktop directory path.
|
||||
///
|
||||
/// Please note that this is not the same as the `%USERPROFILE%\Desktop`!
|
||||
/// The desktop location can be redirected to a different location, e.g. OneDrive.
|
||||
pub fn desktop() -> Result<PathBuf> {
|
||||
dirs::desktop_dir().context("Failed to get the local user's Desktop directory path.")
|
||||
}
|
||||
|
||||
/// Start Menu programs location for the local user.
|
||||
///
|
||||
/// By default, this is `%APPDATA%\Microsoft\Windows\Start Menu\Programs`.
|
||||
pub fn start_menu_programs() -> Result<PathBuf> {
|
||||
known_folder(Shell::FOLDERID_Programs)
|
||||
.context("Failed to get the local user's Start Menu programs directory path.")
|
||||
}
|
||||
|
||||
/// The directory intended for the installation of user-specific non-roaming applications.
|
||||
///
|
||||
/// E.g. `C:\Users\username\AppData\Local\Programs`.
|
||||
///
|
||||
/// # Errors
|
||||
/// This function will return an error if the directory does not exist. This happens on brand new
|
||||
/// Windows installations or new user profiles.
|
||||
pub fn user_program_files() -> Result<PathBuf> {
|
||||
known_folder(Shell::FOLDERID_UserProgramFiles)
|
||||
.context("Failed to get the local user's Program Files directory path.")
|
||||
}
|
||||
|
||||
/// The local application data directory.
|
||||
///
|
||||
/// By default, this is `%LOCALAPPDATA%`, being same as `%USERPROFILE%\AppData\Local`.
|
||||
/// For example, `C:\Users\username\AppData\Local`.
|
||||
pub fn local_app_data() -> Result<PathBuf> {
|
||||
known_folder(Shell::FOLDERID_LocalAppData)
|
||||
.context("Failed to get the local user's Local AppData directory path.")
|
||||
}
|
||||
|
||||
/// Query WinAPI for the path of a known folder.
|
||||
///
|
||||
/// Please refer to the [official documentation](https://learn.microsoft.com/en-us/windows/win32/shell/knownfolderid#constants) for the list of available folders.
|
||||
|
||||
#[context("Failed to get the path of a known folder by GUID {folder_id:?}.")]
|
||||
pub fn known_folder(folder_id: windows::core::GUID) -> Result<PathBuf> {
|
||||
#[cfg(windows)]
|
||||
unsafe {
|
||||
let path = Shell::SHGetKnownFolderPath(&folder_id, default(), None)?;
|
||||
let ret = OsString::from_wide(path.as_wide());
|
||||
windows::Win32::System::Com::CoTaskMemFree(Some(path.0 as *mut c_void));
|
||||
Ok(ret.into())
|
||||
}
|
||||
#[cfg(not(windows))]
|
||||
panic!("Not supported on non-Windows platforms.")
|
||||
}
|
||||
|
||||
/// Notify shell that file associations or registered protocol have changed.
|
||||
///
|
||||
/// This is needed to make the changes visible without restarting the system. See:
|
||||
/// * https://learn.microsoft.com/en-us/windows/win32/shell/fa-file-types#setting-optional-subkeys-and-file-type-extension-attributes
|
||||
/// * https://learn.microsoft.com/en-us/windows/win32/api/shlobj_core/nf-shlobj_core-shchangenotify
|
||||
pub fn refresh_file_associations() {
|
||||
// All direct WinAPI calls are unsafe (Rust-wise), however this particular one should never be
|
||||
// able to cause any harm. It cannot even fail API-wise (it returns `void`).
|
||||
unsafe {
|
||||
Shell::SHChangeNotify(Shell::SHCNE_ASSOCCHANGED, Shell::SHCNF_FLAGS(0), None, None);
|
||||
}
|
||||
}
|
||||
|
||||
/// Application-defined resource (raw data).
|
||||
///
|
||||
/// See https://learn.microsoft.com/en-us/windows/win32/menurc/resource-types
|
||||
pub const RT_RCDATA: PCWSTR = PCWSTR(10 as _);
|
||||
|
||||
|
||||
/// Path to icon, as used in various places in registry (e.g. `DefaultIcon`).
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Icon {
|
||||
/// Path to the executable (or DLL) containing the icon.
|
||||
pub executable_path: PathBuf,
|
||||
/// Index of the icon in the executable.
|
||||
pub icon_index: u32,
|
||||
}
|
||||
|
||||
impl Display for Icon {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, r#""{}",{}"#, self.executable_path.display(), self.icon_index)
|
||||
}
|
||||
}
|
||||
|
||||
impl ToRegValue for Icon {
|
||||
fn to_reg_value(&self) -> RegValue {
|
||||
self.to_string().to_reg_value()
|
||||
}
|
||||
}
|
||||
|
||||
impl Icon {
|
||||
/// Use the first icon in the given PE file.
|
||||
pub fn new(executable_path: impl Into<PathBuf>) -> Self {
|
||||
Self { executable_path: executable_path.into(), icon_index: 0 }
|
||||
}
|
||||
|
||||
/// Store under the `DefaultIcon` subkey of the registry key.
|
||||
pub fn write_default_icon_subkey(&self, key: &RegKey) -> Result {
|
||||
let (icon_key, _disposition) = registry::create_subkey(key, "DefaultIcon")?;
|
||||
registry::set_value(&icon_key, "", self)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// A simple command that can be used by the shell to open a file/url with our application.
|
||||
///
|
||||
/// It will simply start the application, giving the file/url as the first argument.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PlainOpenCommand {
|
||||
/// Path to the executable.
|
||||
pub executable_path: PathBuf,
|
||||
}
|
||||
|
||||
impl Display for PlainOpenCommand {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
// Note that the quotes around both paths are required for paths with spaces.
|
||||
write!(f, r#""{}" "%1""#, self.executable_path.display())
|
||||
}
|
||||
}
|
||||
|
||||
impl ToRegValue for PlainOpenCommand {
|
||||
fn to_reg_value(&self) -> RegValue {
|
||||
self.to_string().to_reg_value()
|
||||
}
|
||||
}
|
||||
|
||||
impl PlainOpenCommand {
|
||||
/// Create a new command that will open the given executable.
|
||||
pub fn new(executable_path: impl Into<PathBuf>) -> Self {
|
||||
Self { executable_path: executable_path.into() }
|
||||
}
|
||||
|
||||
/// Set this as a shell command to open given entity (represented by the key).
|
||||
///
|
||||
/// The command will be stored under the `shell\open\command` subkey of the given key.
|
||||
#[context("Failed to set shell open command.")]
|
||||
pub fn set_as_shell_open_command(&self, key: &RegKey) -> Result {
|
||||
let command_key = registry::create_subkey(key, r"shell\open\command")?.0;
|
||||
registry::set_value(&command_key, "", self)
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the binary payload of the installer that was compiled into the executable.
|
||||
pub fn get_installer_payload() -> Result<&'static [u8]> {
|
||||
resource::get_binary(INSTALLER_PAYLOAD_ID).with_context(|| format!("Failed to get the installer payload. Was {ENSO_INSTALL_ARCHIVE_PATH} defined during the build?"))
|
||||
}
|
65
build/install/src/win/app_paths.rs
Normal file
65
build/install/src/win/app_paths.rs
Normal file
@ -0,0 +1,65 @@
|
||||
//! Helpers for managing known application paths on Windows.
|
||||
//!
|
||||
//! The `App Paths` registry key is necessary for the Windows shell to find the
|
||||
//! application executable when it is invoked by name.
|
||||
//!
|
||||
//! See https://docs.microsoft.com/en-us/windows/win32/shell/app-registration
|
||||
|
||||
use crate::prelude::*;
|
||||
use winreg::enums::*;
|
||||
|
||||
|
||||
|
||||
/// The registry key path for the `App Paths` in Windows, relative to `HKEY_CURRENT_USER`.
|
||||
pub const APP_PATHS: &str = r"Software\Microsoft\Windows\CurrentVersion\App Paths";
|
||||
|
||||
|
||||
/// Information stored in the `App Paths` registry key for an application.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct AppPathInfo {
|
||||
/// The path to the application executable.
|
||||
///
|
||||
/// It is used both for:
|
||||
/// * executable filename (which will allow shell to find the executable when it is invoked by
|
||||
/// name, e.g. when using `Win+R`)
|
||||
/// * full qualified executable path that will be used to launch the application.
|
||||
pub executable_path: PathBuf,
|
||||
|
||||
/// Additional directories that will be prepended to the `PATH` environment variable when
|
||||
/// launching the application through the `ShellExecuteEx` API.
|
||||
pub additional_directories: Vec<PathBuf>,
|
||||
}
|
||||
|
||||
impl AppPathInfo {
|
||||
/// Create a new `AppPathInfo` instance.
|
||||
pub fn new(executable_path: impl Into<PathBuf>) -> Self {
|
||||
Self {
|
||||
executable_path: executable_path.into(),
|
||||
// By default don't add anything to path, as we don't want to rely on any
|
||||
// Windows shell-specific behavior.
|
||||
additional_directories: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
/// Write the application path information to the registry.
|
||||
pub fn write_to_registry(&self) -> Result {
|
||||
let executable_name = self.executable_path.try_file_name()?.as_str();
|
||||
let app_paths = crate::win::registry::open_subkey_with_flags(
|
||||
&RegKey::predef(HKEY_CURRENT_USER),
|
||||
APP_PATHS,
|
||||
KEY_READ | KEY_WRITE,
|
||||
)?;
|
||||
let (exe_key, _) = crate::win::registry::create_subkey(&app_paths, executable_name)?;
|
||||
crate::win::registry::set_value(&exe_key, "", &self.executable_path.as_str())?;
|
||||
if !self.additional_directories.is_empty() {
|
||||
let path = self
|
||||
.additional_directories
|
||||
.iter()
|
||||
.map(|p| p.as_str())
|
||||
.collect::<Vec<_>>()
|
||||
.join(";");
|
||||
crate::win::registry::set_value(&exe_key, "Path", &path)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
195
build/install/src/win/prog_id.rs
Normal file
195
build/install/src/win/prog_id.rs
Normal file
@ -0,0 +1,195 @@
|
||||
//! Code for handling [programmatic identifiers](https://docs.microsoft.com/en-us/windows/win32/com/programmatic-identifiers).
|
||||
//!
|
||||
//! Basically, there are two kinds of programmatic identifiers:
|
||||
//! * File extension associations;
|
||||
//! * URL protocol associations.
|
||||
//!
|
||||
//! Programmatic identifiers are stored in the Windows registry under the:
|
||||
//! * `HKEY_CURRENT_USER\Software\Classes` key for the current user;
|
||||
//! * `HKEY_LOCAL_MACHINE\Software\Classes` key for all users.
|
||||
//!
|
||||
//! `HKEY_CLASSES_ROOT` is a merged view of these two keys.
|
||||
|
||||
use crate::prelude::*;
|
||||
use winreg::enums::*;
|
||||
|
||||
use crate::win::registry::create_subkey;
|
||||
|
||||
|
||||
|
||||
/// A set of broad categories for file types hard-coded into Windows.
|
||||
///
|
||||
/// See: https://learn.microsoft.com/en-us/windows/win32/api/shtypes/ne-shtypes-perceived
|
||||
#[derive(Debug, Clone, Copy, strum::AsRefStr)]
|
||||
// [mwu] I have no idea if these are case-sensitive but why would we want to risk it?
|
||||
#[strum(serialize_all = "snake_case")]
|
||||
pub enum PerceivedType {
|
||||
Folder,
|
||||
Text,
|
||||
Image,
|
||||
Audio,
|
||||
Video,
|
||||
Compressed,
|
||||
Document,
|
||||
System,
|
||||
Application,
|
||||
Gamemedia,
|
||||
Contacts,
|
||||
}
|
||||
|
||||
impl PerceivedType {
|
||||
/// Deduce `PerceivedType` from MIME type.
|
||||
pub fn from_mime_type(mime_type: &str) -> Result<Self> {
|
||||
Ok(match mime_type {
|
||||
"text/plain" => Self::Text,
|
||||
"application/gzip" | "application/zip" => Self::Compressed,
|
||||
_ => bail!("MIME type without a corresponding perceived type: '{mime_type}'."),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// The file extension description.
|
||||
///
|
||||
/// Apart from some basic information, it directs to the ProgID of the file type.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct FileExtension {
|
||||
/// The file extension including the leading dot, e.g. `.enso`.
|
||||
pub extension: String,
|
||||
/// The `ProgID` of the file type.
|
||||
pub prog_id: String,
|
||||
/// The MIME type of the file.
|
||||
pub mime_type: String,
|
||||
/// A broad category for the file type, e.g. `text`.
|
||||
pub perceived_type: PerceivedType,
|
||||
}
|
||||
|
||||
impl FileExtension {
|
||||
/// Write information about the file extension to the Windows registry.
|
||||
///
|
||||
/// Note that this only writes a reference to the ProgID of the file type. The file type
|
||||
/// itself must be registered separately using [`FileType::register`].
|
||||
pub fn register(&self) -> Result {
|
||||
let classes = crate::win::classes_key()?;
|
||||
// https://learn.microsoft.com/en-us/windows/win32/shell/fa-file-types#setting-optional-subkeys-and-file-type-extension-attributes
|
||||
let (file_ext_key, _disposition) = classes.create_subkey(&self.extension)?;
|
||||
file_ext_key.set_value("", &self.prog_id)?;
|
||||
file_ext_key.set_value("Content Type", &self.mime_type)?;
|
||||
file_ext_key.set_value("PerceivedType", &"text")?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// The Programmatic Identifier (ProgID) of a file type.
|
||||
///
|
||||
/// These are associated with file extensions in the Windows registry and describe how to
|
||||
/// open files of that type. More than one file extension can be associated with a single
|
||||
/// ProgID.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct FileType {
|
||||
/// The absolute path to the application executable.
|
||||
pub application_path: PathBuf,
|
||||
/// The `ProgID` of the file type - a unique identifier for the file type.
|
||||
pub prog_id: String,
|
||||
/// The friendly name of the file type.
|
||||
pub friendly_name: String,
|
||||
/// The text to display in the info popup when hovering over a file of this type.
|
||||
pub info_tip: String,
|
||||
}
|
||||
|
||||
impl FileType {
|
||||
/// Write information about the file type to the Windows registry.
|
||||
pub fn register(&self) -> Result {
|
||||
let classes = crate::win::classes_key()?;
|
||||
let path_str = self.application_path.as_str();
|
||||
|
||||
// https://learn.microsoft.com/en-us/windows/win32/shell/fa-progids
|
||||
// Describe the Programmatic Identifier (ProgID) of the file type.
|
||||
let (prog_id_key, _disposition) = classes.create_subkey(&self.prog_id)?;
|
||||
prog_id_key.set_value("", &self.friendly_name)?;
|
||||
prog_id_key.set_value("FriendlyTypeName", &self.friendly_name)?;
|
||||
prog_id_key.set_value("InfoTip", &self.info_tip)?;
|
||||
// 0 is the index of the icon in the executable (i.e. the first icon).
|
||||
// See: https://docs.microsoft.com/en-us/windows/win32/shell/fa-file-types#setting-optional-subkeys-and-file-type-extension-attributes
|
||||
prog_id_key.create_subkey("DefaultIcon")?.0.set_value("", &format!(r#""{path_str}",0"#))?;
|
||||
prog_id_key
|
||||
.create_subkey(r"shell\open\command")?
|
||||
.0
|
||||
// Note that the quotes around both paths are required for paths with spaces.
|
||||
.set_value("", &format!(r#""{path_str}" "%1""#))?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Delete given programmatic identifier from the Windows registry.
|
||||
#[context("Failed to delete ProgID `{}`.", prog_id)]
|
||||
pub fn delete(prog_id: &str) -> Result {
|
||||
// Must be non-empty, or we will delete the entire `Classes` key.
|
||||
ensure!(!prog_id.is_empty(), "Programmatic identifier must not be empty.");
|
||||
let classes = crate::win::classes_key()?;
|
||||
classes.delete_subkey_all(prog_id)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Information about a URL protocol.
|
||||
#[derive(Debug)]
|
||||
pub struct ProtocolInfo {
|
||||
/// The protocol sheme name, e.g. `enso`.
|
||||
pub protocol: String,
|
||||
/// The icon for the protocol.
|
||||
pub icon: crate::win::Icon,
|
||||
/// The command to run when opening the protocol's URL.
|
||||
pub command: crate::win::PlainOpenCommand,
|
||||
/// Display name of the protocol.
|
||||
pub display_name: Option<String>,
|
||||
}
|
||||
|
||||
impl ProtocolInfo {
|
||||
pub fn new(protocol: impl Into<String>, executable_path: impl Into<PathBuf>) -> Self {
|
||||
let protocol = protocol.into();
|
||||
let display_name = Some(format!("URL:{protocol} protocol"));
|
||||
let executable_path = executable_path.into();
|
||||
Self {
|
||||
protocol,
|
||||
icon: crate::win::Icon::new(&executable_path),
|
||||
command: crate::win::PlainOpenCommand::new(executable_path),
|
||||
display_name,
|
||||
}
|
||||
}
|
||||
|
||||
/// Write information about the protocol to the Windows registry.
|
||||
pub fn register(&self) -> Result {
|
||||
let classes = crate::win::classes_key()?;
|
||||
let (protocol_key, _) = create_subkey(&classes, &self.protocol)?;
|
||||
protocol_key.set_value("URL Protocol", &"")?;
|
||||
if let Some(display_name) = &self.display_name {
|
||||
protocol_key.set_value("", display_name)?;
|
||||
}
|
||||
self.icon.write_default_icon_subkey(&protocol_key)?;
|
||||
self.command.set_as_shell_open_command(&protocol_key)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Set application as the default handler for the given URL protocol, e.g. `enso://`.
|
||||
///
|
||||
/// This is necessary for the deep linking to work.
|
||||
pub fn register_url_protocol(executable_path: &Path, protocol: &str) -> Result {
|
||||
// See https://learn.microsoft.com/en-us/windows/win32/shell/app-registration
|
||||
|
||||
// Register the URL protocol.
|
||||
let (url_key, _) = RegKey::predef(HKEY_CURRENT_USER)
|
||||
.open_subkey_with_flags(r"Software\Classes", KEY_READ | KEY_WRITE)
|
||||
.context("Failed to open `HKEY_CURRENT_USER\\Software\\Classes` key.")?
|
||||
.create_subkey(protocol)
|
||||
.with_context(|| format!(r#"Failed to create subkey for protocol `{protocol}`"#))?;
|
||||
|
||||
url_key.set_value("", &format!("URL:{protocol}"))?;
|
||||
url_key.set_value("URL Protocol", &"")?;
|
||||
|
||||
let (command_key, _) = url_key
|
||||
.create_subkey(r"shell\open\command")
|
||||
.with_context(|| format!(r#"Failed to create subkey for protocol: {protocol}"#))?;
|
||||
command_key.set_value("", &format!(r#""{}" "%1""#, executable_path.display()))?;
|
||||
|
||||
Ok(())
|
||||
}
|
31
build/install/src/win/registry.rs
Normal file
31
build/install/src/win/registry.rs
Normal file
@ -0,0 +1,31 @@
|
||||
//! Wrapper around the `winreg` crate functions that provides better error messages.
|
||||
|
||||
use crate::prelude::*;
|
||||
|
||||
use winreg::enums::RegDisposition;
|
||||
|
||||
|
||||
|
||||
/// Wrapper around [`RegKey::delete_subkey_all`] that provides better error messages.
|
||||
pub fn delete_subkey_all(key: &RegKey, subkey: &str) -> Result {
|
||||
key.delete_subkey_all(subkey)
|
||||
.with_context(|| format!("Failed to delete subkey `{subkey}` in `{key:?}`."))
|
||||
}
|
||||
|
||||
/// Wrapper around [`RegKey::create_subkey`] that provides better error messages.
|
||||
pub fn create_subkey(key: &RegKey, subkey: &str) -> Result<(RegKey, RegDisposition)> {
|
||||
key.create_subkey(subkey)
|
||||
.with_context(|| format!("Failed to create subkey `{subkey}` in `{key:?}`."))
|
||||
}
|
||||
|
||||
/// Wrapper around [`RegKey::open_subkey_with_flags`] that provides better error messages.
|
||||
pub fn open_subkey_with_flags(key: &RegKey, subkey: &str, flags: u32) -> Result<RegKey> {
|
||||
key.open_subkey_with_flags(subkey, flags)
|
||||
.with_context(|| format!("Failed to open subkey `{subkey}` in `{key:?}`."))
|
||||
}
|
||||
|
||||
/// Wrapper around [`RegKey::set_value`] that provides better error messages.
|
||||
pub fn set_value(key: &RegKey, name: &str, value: &impl ToRegValue) -> Result {
|
||||
key.set_value(name, value)
|
||||
.with_context(|| format!("Failed to set value `{name}` in `{key:?}`."))
|
||||
}
|
28
build/install/src/win/resource.rs
Normal file
28
build/install/src/win/resource.rs
Normal file
@ -0,0 +1,28 @@
|
||||
//! Utilities for working with Resources embedded in Windows executables.
|
||||
|
||||
use crate::prelude::*;
|
||||
|
||||
use windows::core::HSTRING;
|
||||
use windows::Win32::Foundation;
|
||||
use windows::Win32::System::LibraryLoader;
|
||||
|
||||
|
||||
|
||||
/// Get binary resource embedded in the current executable.
|
||||
///
|
||||
/// The resource must be compiled into the current executable as `RCDATA`.
|
||||
#[context("Failed to get binary resource named `{name}`.")]
|
||||
pub fn get_binary(name: &str) -> Result<&'static [u8]> {
|
||||
unsafe {
|
||||
// Clear error, so any `GetLastError` call after this one will return actual error from the
|
||||
// subsequent calls.
|
||||
Foundation::SetLastError(Foundation::WIN32_ERROR::default());
|
||||
let resource =
|
||||
LibraryLoader::FindResourceW(None, &HSTRING::from(name), crate::win::RT_RCDATA);
|
||||
Foundation::GetLastError().with_context(|| format!("Failed to find resource: {name:?}"))?;
|
||||
let global = LibraryLoader::LoadResource(None, resource).unwrap();
|
||||
let data = LibraryLoader::LockResource(global);
|
||||
let size = LibraryLoader::SizeofResource(None, resource);
|
||||
Ok(std::slice::from_raw_parts(data as *const u8, size as _))
|
||||
}
|
||||
}
|
65
build/install/src/win/shortcut.rs
Normal file
65
build/install/src/win/shortcut.rs
Normal file
@ -0,0 +1,65 @@
|
||||
//! Utilities for managing start menu and desktop shortcuts.
|
||||
|
||||
use crate::prelude::*;
|
||||
|
||||
use ide_ci::env::consts::SHORTCUT_SUFFIX;
|
||||
|
||||
|
||||
|
||||
/// Location of the shortcut.
|
||||
#[derive(Copy, Clone, Debug, Display)]
|
||||
pub enum Location {
|
||||
/// Local user's Start Menu shortcut (in the Programs folder).
|
||||
Menu,
|
||||
/// Local user's Desktop shortcut.
|
||||
Desktop,
|
||||
}
|
||||
|
||||
impl Location {
|
||||
/// Get the directory where the shortcut should be created.
|
||||
#[context("Failed to get {self} shortcut location.")]
|
||||
pub fn directory(&self) -> Result<PathBuf> {
|
||||
match self {
|
||||
Self::Menu => Ok(crate::win::start_menu_programs()?),
|
||||
Self::Desktop => Ok(crate::win::desktop()?),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the shortcut file path.
|
||||
#[context("Failed to get {self} shortcut '{name}' path.")]
|
||||
pub fn shortcut_path(&self, name: &str) -> Result<PathBuf> {
|
||||
Ok(self.directory()?.join(name).with_extension(SHORTCUT_SUFFIX))
|
||||
}
|
||||
|
||||
/// Create shortcut that links to target.
|
||||
#[context("Failed to create {self} shortcut `{}`.", name)]
|
||||
pub fn create_shortcut(&self, name: &str, target: &Path) -> Result {
|
||||
let shortcut_path = self.shortcut_path(name)?;
|
||||
create_shortcut_customized(&shortcut_path, target, |link| {
|
||||
link.set_name(Some(name.into()));
|
||||
})
|
||||
}
|
||||
|
||||
/// Remove the shortcut.
|
||||
#[context("Failed to remove {self} shortcut `{}`.", name)]
|
||||
pub fn remove_shortcut(&self, name: &str) -> Result {
|
||||
let shortcut_path = self.shortcut_path(name)?;
|
||||
ide_ci::fs::remove_file_if_exists(shortcut_path)
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a Windows shortcut (`.lnk` file).
|
||||
pub fn create_shortcut_customized(
|
||||
shortcut_path: &Path,
|
||||
target: &Path,
|
||||
f: impl FnOnce(&mut mslnk::ShellLink),
|
||||
) -> Result {
|
||||
// Paths with verbatim prefix (i.e. `\\?\`) are not supported by the Windows Shell API.
|
||||
let target = target.without_verbatim_prefix();
|
||||
info!("Creating shortcut {} -> {}", shortcut_path.display(), target.display());
|
||||
ide_ci::fs::create_parent_dir_if_missing(shortcut_path)?;
|
||||
let mut link = mslnk::ShellLink::new(target)?;
|
||||
f(&mut link);
|
||||
link.create_lnk(shortcut_path)?;
|
||||
Ok(())
|
||||
}
|
23
build/install/src/win/ui.rs
Normal file
23
build/install/src/win/ui.rs
Normal file
@ -0,0 +1,23 @@
|
||||
//! UI-related utilities for the Windows installer/uninstaller.
|
||||
|
||||
use crate::prelude::*;
|
||||
|
||||
|
||||
|
||||
/// [Sets up logging](crate::setup_logging). Handles any errors by showing a fatal message box.
|
||||
///
|
||||
/// # Panics
|
||||
/// This function panics if it fails to setup logging. The panic will follow immediately after the
|
||||
/// error message box is closed.
|
||||
pub fn setup_logging_or_fatal(app_name: &str, dialog_title: &str) -> PathBuf {
|
||||
match crate::setup_logging(app_name) {
|
||||
Ok(logfile) => {
|
||||
info!("Logging to: {}", logfile.display());
|
||||
logfile
|
||||
}
|
||||
Err(err) => {
|
||||
let message = format!("Failed to create a log file: {err:?}");
|
||||
native_windows_gui::fatal_message(dialog_title, &message);
|
||||
}
|
||||
}
|
||||
}
|
131
build/install/src/win/uninstall.rs
Normal file
131
build/install/src/win/uninstall.rs
Normal file
@ -0,0 +1,131 @@
|
||||
//! Uninstaller information for Windows.
|
||||
//!
|
||||
//! These functions allow you to write uninstall information to the registry, and remove it when the
|
||||
//! application is uninstalled.
|
||||
|
||||
use crate::prelude::*;
|
||||
use winreg::enums::*;
|
||||
|
||||
|
||||
|
||||
/// The path to the Uninstall subkey of the registry.
|
||||
const UNINSTALL_KEY_PATH: &str = "Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall";
|
||||
|
||||
/// Struct representing the uninstall information stored in the `Uninstall` key.
|
||||
#[derive(Debug)]
|
||||
pub struct UninstallInfo {
|
||||
/// The name of the application as it appears in the Add/Remove Programs dialog.
|
||||
pub display_name: String,
|
||||
|
||||
/// The command that will be executed when the user chooses to uninstall the application.
|
||||
pub uninstall_string: String,
|
||||
|
||||
/// The path to the icon that will be displayed next to the application in the Add/Remove
|
||||
/// Programs dialog.
|
||||
pub display_icon: Option<String>,
|
||||
|
||||
/// The version of the application.
|
||||
pub display_version: Option<String>,
|
||||
|
||||
/// The publisher of the application.
|
||||
pub publisher: Option<String>,
|
||||
|
||||
/// A URL for the About button in the Add/Remove Programs dialog.
|
||||
pub url_info_about: Option<String>,
|
||||
|
||||
/// A URL for the Update button in the Add/Remove Programs dialog.
|
||||
pub url_update_info: Option<String>,
|
||||
|
||||
/// A URL for the Help button in the Add/Remove Programs dialog.
|
||||
pub help_link: Option<String>,
|
||||
|
||||
/// The installation location of the application.
|
||||
pub install_location: Option<String>,
|
||||
|
||||
/// The date the application was installed.
|
||||
pub install_date: Option<String>,
|
||||
|
||||
/// The size of the application in kibibytes.
|
||||
pub estimated_size_kib: Option<u32>,
|
||||
}
|
||||
|
||||
impl UninstallInfo {
|
||||
pub fn new(display_name: impl Into<String>, uninstall_string: impl Into<String>) -> Self {
|
||||
Self {
|
||||
display_name: display_name.into(),
|
||||
uninstall_string: uninstall_string.into(),
|
||||
display_icon: None,
|
||||
display_version: None,
|
||||
publisher: None,
|
||||
url_info_about: None,
|
||||
url_update_info: None,
|
||||
help_link: None,
|
||||
install_location: None,
|
||||
install_date: None,
|
||||
estimated_size_kib: None,
|
||||
}
|
||||
}
|
||||
|
||||
#[context("Failed to write '{app_key}' uninstall information to the registry.")]
|
||||
pub fn write_to_registry(&self, app_key: &str) -> Result {
|
||||
trace!("Writing uninstall information to the registry: {self:#?}");
|
||||
let uninstall_key = open_uninstall_key()?;
|
||||
|
||||
// Create a new key for our application
|
||||
let (app_uninstall_key, _) = uninstall_key.create_subkey(app_key)?;
|
||||
|
||||
// Set the necessary values
|
||||
app_uninstall_key.set_value("DisplayName", &self.display_name)?;
|
||||
app_uninstall_key.set_value("UninstallString", &self.uninstall_string)?;
|
||||
|
||||
if let Some(display_icon) = &self.display_icon {
|
||||
app_uninstall_key.set_value("DisplayIcon", display_icon)?;
|
||||
}
|
||||
if let Some(display_version) = &self.display_version {
|
||||
app_uninstall_key.set_value("DisplayVersion", display_version)?;
|
||||
}
|
||||
if let Some(publisher) = &self.publisher {
|
||||
app_uninstall_key.set_value("Publisher", publisher)?;
|
||||
}
|
||||
if let Some(url_info_about) = &self.url_info_about {
|
||||
app_uninstall_key.set_value("URLInfoAbout", url_info_about)?;
|
||||
}
|
||||
if let Some(url_update_info) = &self.url_update_info {
|
||||
app_uninstall_key.set_value("URLUpdateInfo", url_update_info)?;
|
||||
}
|
||||
if let Some(help_link) = &self.help_link {
|
||||
app_uninstall_key.set_value("HelpLink", help_link)?;
|
||||
}
|
||||
if let Some(install_location) = &self.install_location {
|
||||
app_uninstall_key.set_value("InstallLocation", install_location)?;
|
||||
}
|
||||
if let Some(install_date) = &self.install_date {
|
||||
app_uninstall_key.set_value("InstallDate", install_date)?;
|
||||
}
|
||||
if let Some(estimated_size) = self.estimated_size_kib {
|
||||
app_uninstall_key.set_value("EstimatedSize", &estimated_size)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Open the `Uninstall` key in the registry.
|
||||
pub fn open_uninstall_key() -> Result<RegKey> {
|
||||
RegKey::predef(HKEY_CURRENT_USER)
|
||||
.open_subkey_with_flags(UNINSTALL_KEY_PATH, KEY_READ | KEY_WRITE)
|
||||
.with_context(|| {
|
||||
format!("Failed to open the uninstall key in the registry: {UNINSTALL_KEY_PATH}")
|
||||
})
|
||||
}
|
||||
|
||||
/// Removes the uninstall information from the registry.
|
||||
#[context("Failed to remove '{app_key}' uninstall information from the registry.")]
|
||||
pub fn remove_from_registry(app_key: &str) -> Result {
|
||||
let uninstall_key = open_uninstall_key()?;
|
||||
uninstall_key.delete_subkey_all(app_key).with_context(|| {
|
||||
format!(
|
||||
"Failed to delete the '{app_key}' subkey from the '{UNINSTALL_KEY_PATH}' in the registry."
|
||||
)
|
||||
})
|
||||
}
|
23
build/install/uninstaller/Cargo.toml
Normal file
23
build/install/uninstaller/Cargo.toml
Normal file
@ -0,0 +1,23 @@
|
||||
[package]
|
||||
name = "enso-uninstaller"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
anyhow = { workspace = true }
|
||||
enso-build-base = { path = "../../base" }
|
||||
enso-build = { path = "../../build" }
|
||||
enso-install = { path = ".." }
|
||||
enso-install-config = { path = "../config" }
|
||||
ide-ci = { path = "../../ci_utils" }
|
||||
self-replace = "1.3.7"
|
||||
sysinfo = { workspace = true }
|
||||
tokio = { workspace = true }
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
native-windows-gui = { workspace = true }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
35
build/install/uninstaller/src/main.rs
Normal file
35
build/install/uninstaller/src/main.rs
Normal file
@ -0,0 +1,35 @@
|
||||
#![windows_subsystem = "windows"] // Do not display a console window when running the uninstaller.
|
||||
|
||||
use enso_install::prelude::*;
|
||||
|
||||
|
||||
// ==============
|
||||
// === Export ===
|
||||
// ==============
|
||||
|
||||
#[cfg(windows)]
|
||||
pub mod win;
|
||||
|
||||
|
||||
|
||||
#[cfg(windows)]
|
||||
#[tokio::main]
|
||||
pub async fn main() -> Result {
|
||||
win::main().await
|
||||
}
|
||||
|
||||
|
||||
#[cfg(not(windows))]
|
||||
fn main() -> Result {
|
||||
bail!("This uninstaller is only supported on Windows.")
|
||||
}
|
||||
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#[test]
|
||||
fn uninstaller_name_matches() {
|
||||
// Make sure the uninstaller expected filename matches the package name.
|
||||
assert_eq!(enso_install_config::UNINSTALLER_NAME, env!("CARGO_PKG_NAME"));
|
||||
}
|
||||
}
|
171
build/install/uninstaller/src/win.rs
Normal file
171
build/install/uninstaller/src/win.rs
Normal file
@ -0,0 +1,171 @@
|
||||
use enso_install::prelude::*;
|
||||
|
||||
use enso_install::is_already_running;
|
||||
use enso_install::locked_installation_lock;
|
||||
use enso_install::sanitized_electron_builder_config;
|
||||
|
||||
|
||||
|
||||
/// The parent directory of this (uninstaller) executable.
|
||||
///
|
||||
/// This is a good candidate for the install directory of Enso.
|
||||
fn parent_directory() -> Result<PathBuf> {
|
||||
let exe_path = ide_ci::env::current_exe()?;
|
||||
exe_path.try_parent().map(Into::into)
|
||||
}
|
||||
|
||||
/// Delete the uninstaller executable.
|
||||
///
|
||||
/// This uses the [`self_replace`] crate to delete the executable on Windows. Running executable
|
||||
/// cannot be deleted on Windows using the ordinary means.
|
||||
///
|
||||
/// This must be invoked before deleting the install directory, if the uninstaller is located in the
|
||||
/// install directory. Otherwise, the executable makes the directory non-deletable.
|
||||
fn self_delete(parent_path: &Path) -> Result {
|
||||
self_replace::self_delete_outside_path(parent_path).with_context(|| {
|
||||
format!(
|
||||
"Failed to delete the Enso executable. \
|
||||
Please delete the file manually: {}",
|
||||
parent_path.display()
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
/// Handle an error, logging it and adding it to the list of errors.
|
||||
fn handle_error<T>(errors: &mut Vec<anyhow::Error>, result: Result<T>) -> Option<T> {
|
||||
match result {
|
||||
Err(error) => {
|
||||
error!("Encountered an error: {error}.");
|
||||
errors.push(error);
|
||||
None
|
||||
}
|
||||
Ok(value) => Some(value),
|
||||
}
|
||||
}
|
||||
|
||||
/// Show a dialog with an error message and panic.
|
||||
fn show_dialog_and_panic(error: &anyhow::Error) -> ! {
|
||||
error!("Encountered an error: {error:?}.");
|
||||
native_windows_gui::fatal_message(
|
||||
"Uninstallation failed",
|
||||
&format!("Encountered an error: {error}", error = error),
|
||||
);
|
||||
}
|
||||
|
||||
pub async fn main() -> Result {
|
||||
let dialog_title = format!("{} installer", sanitized_electron_builder_config().product_name);
|
||||
let logfile =
|
||||
enso_install::win::ui::setup_logging_or_fatal(env!("CARGO_PKG_NAME"), &dialog_title);
|
||||
|
||||
// Show a yes-no modal dialog.
|
||||
let message =
|
||||
format!("Do you want to uninstall {}?", sanitized_electron_builder_config().product_name);
|
||||
|
||||
let params = native_windows_gui::MessageParams {
|
||||
title: &dialog_title,
|
||||
content: &message,
|
||||
buttons: native_windows_gui::MessageButtons::YesNo,
|
||||
icons: native_windows_gui::MessageIcons::Question,
|
||||
};
|
||||
match native_windows_gui::message(¶ms) {
|
||||
native_windows_gui::MessageChoice::Yes => (),
|
||||
native_windows_gui::MessageChoice::No => {
|
||||
info!("User chose not to uninstall.");
|
||||
return Ok(());
|
||||
}
|
||||
_ => bail!("Unexpected message box result."),
|
||||
}
|
||||
|
||||
|
||||
let mut errors = vec![];
|
||||
let _guard = match locked_installation_lock() {
|
||||
Ok(guard) => guard,
|
||||
Err(error) => show_dialog_and_panic(&error),
|
||||
};
|
||||
|
||||
// Unwrap is safe, because the uninstaller path (being an executable) will never be a root.
|
||||
let install_dir = parent_directory().unwrap();
|
||||
|
||||
// Check if there is already running instance of Enso or any of its subprograms.
|
||||
let already_running = sysinfo::get_current_pid()
|
||||
.map_err(|text| anyhow!("Failed to get current process ID: {text}"))
|
||||
.and_then(|my_pid| {
|
||||
is_already_running(&install_dir, &[my_pid])
|
||||
.context("Failed to check if already running.")
|
||||
});
|
||||
match already_running {
|
||||
Ok(Some(message)) => show_dialog_and_panic(&anyhow!("{message}")),
|
||||
Ok(None) => (),
|
||||
Err(error) => warn!("Failed to check if there is already running instance: {error:?}"),
|
||||
};
|
||||
|
||||
// Make sure that Enso.exe is in the same directory as this uninstaller. This is to prevent
|
||||
// situation where just the uninstaller binary is placed by accident elsewhere and ends up
|
||||
// deleting the whole directory.
|
||||
let executable_filename = enso_install::executable_filename();
|
||||
let expected_executable = install_dir.join(&executable_filename);
|
||||
let shortcut_name = enso_install::shortcut_name();
|
||||
|
||||
ensure!(
|
||||
expected_executable.exists(),
|
||||
"{} not found in the presumed install directory {}",
|
||||
executable_filename.display(),
|
||||
install_dir.display()
|
||||
);
|
||||
|
||||
info!("Remove Add/Remove Programs entry.");
|
||||
handle_error(
|
||||
&mut errors,
|
||||
enso_install::win::uninstall::remove_from_registry(enso_install::uninstall_key()),
|
||||
);
|
||||
|
||||
info!("Removing self (uninstaller) executable.");
|
||||
handle_error(&mut errors, self_delete(&install_dir));
|
||||
|
||||
info!("Removing install directory.");
|
||||
handle_error(&mut errors, ide_ci::fs::remove_dir_if_exists(&install_dir));
|
||||
|
||||
// Remove prog id but leave file extensions - see https://learn.microsoft.com/en-us/windows/win32/shell/fa-file-types#deleting-registry-information-during-uninstallation
|
||||
for file_association in
|
||||
&sanitized_electron_builder_config().extra_metadata.installer.file_associations
|
||||
{
|
||||
let prog_id = &file_association.prog_id;
|
||||
info!("Removing ProgID `{prog_id}`.");
|
||||
handle_error(&mut errors, enso_install::win::prog_id::delete(prog_id));
|
||||
}
|
||||
|
||||
info!("Removing Start Menu entry.");
|
||||
handle_error(
|
||||
&mut errors,
|
||||
enso_install::win::shortcut::Location::Menu.remove_shortcut(shortcut_name),
|
||||
);
|
||||
|
||||
info!("Removing Desktop shortcut.");
|
||||
handle_error(
|
||||
&mut errors,
|
||||
enso_install::win::shortcut::Location::Desktop.remove_shortcut(shortcut_name),
|
||||
);
|
||||
|
||||
if !errors.is_empty() {
|
||||
error!("Encountered {} errors.", errors.len());
|
||||
for error in &errors {
|
||||
error!(" * {error:?}");
|
||||
}
|
||||
|
||||
let errors_summary = errors.iter().map(|e| format!("{e}")).join("\n *");
|
||||
let plain_message = format!(
|
||||
"Encountered errors during uninstallation. Some files or \
|
||||
registry entries may have been left behind. Please see the log file for details. The file \
|
||||
explorer will be opened at the log file location.\n\nErrors:\n{errors_summary}"
|
||||
);
|
||||
native_windows_gui::error_message("Uninstallation failed", &plain_message);
|
||||
let _ = ide_ci::programs::explorer::show_selected(logfile);
|
||||
bail!("Uninstallation failed.");
|
||||
} else {
|
||||
native_windows_gui::simple_message(
|
||||
"Uninstallation successful",
|
||||
"Enso has been successfully uninstalled.",
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
}
|
@ -16,6 +16,7 @@ use enso_parser::syntax::tree::DocComment;
|
||||
use enso_parser::syntax::tree::TextElement;
|
||||
|
||||
|
||||
|
||||
// ====================================
|
||||
// === Debug Representation Printer ===
|
||||
// ====================================
|
||||
|
1
package-lock.json
generated
1
package-lock.json
generated
@ -187,6 +187,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@electron/notarize": "2.1.0",
|
||||
"@types/node": "^20.10.5",
|
||||
"electron": "25.7.0",
|
||||
"electron-builder": "^22.14.13",
|
||||
"enso-common": "^1.0.0",
|
||||
|
Loading…
Reference in New Issue
Block a user