pifu/nsis/
build.rs

1// Copyright (c) 2021 Xu Shaohua <shaohua@biofan.org>. All rights reserved.
2// Use of this source is governed by General Public License that can be found
3// in the LICENSE file.
4
5use std::fmt::Debug;
6use std::fs::{self, File};
7use std::io::Write;
8use std::path::{Path, PathBuf};
9use std::process::Command;
10
11use super::config::NsisConfig;
12use crate::base::{expand_file_macro, Arch, PlatformTarget};
13use crate::config::{Config, WindowsConfig};
14use crate::error::{Error, ErrorKind};
15
16pub fn build_nsis(conf: &Config, windows_conf: &WindowsConfig, arch: Arch) -> Result<(), Error> {
17    let nsis_conf = if let Some(nsis_conf) = windows_conf.nsis.as_ref() {
18        nsis_conf
19    } else {
20        return Err(Error::new(
21            ErrorKind::InvalidConfError,
22            "`nsis` config not set!",
23        ));
24    };
25
26    if let Some(script) = nsis_conf.script.as_ref() {
27        compile_nsis(&script)
28    } else {
29        let nsis_file = generate_nsis_file(conf, windows_conf, arch, nsis_conf)?;
30        compile_nsis(&nsis_file)
31    }
32}
33
34fn define_header(
35    conf: &Config,
36    arch: Arch,
37    nsis_conf: &NsisConfig,
38    nsis_fd: &mut File,
39) -> Result<(), Error> {
40    // Generate nsi script
41    writeln!(nsis_fd, "# Generated by pifu. DO NOT EDIT!\n")?;
42    writeln!(nsis_fd, "!include \"MUI2.nsh\"\n")?;
43
44    if let Some(include_file) = nsis_conf.include.as_ref() {
45        writeln!(nsis_fd, "!include {:?}\n", fs::canonicalize(include_file)?)?;
46    }
47
48    writeln!(nsis_fd, "Name {}", &conf.metadata.name)?;
49
50    if nsis_conf.unicode {
51        writeln!(nsis_fd, "Unicode True")?;
52    } else {
53        writeln!(nsis_fd, "Unicode False")?;
54    }
55
56    let artifact_name =
57        expand_file_macro(&nsis_conf.artifact_name, conf, arch, PlatformTarget::Nsis)?;
58    writeln!(nsis_fd, r#"OutFile "{artifact_name}""#)?;
59    writeln!(
60        nsis_fd,
61        "SetCompressor /SOLID {}\n",
62        nsis_conf.compress_method
63    )?;
64
65    if nsis_conf.warnings_as_errors {
66        writeln!(nsis_fd, "!define MUI_ABORTWARNING")?;
67    }
68    Ok(())
69}
70
71fn define_icons(nsis_conf: &NsisConfig, nsis_fd: &mut File) -> Result<(), Error> {
72    // Icons
73    writeln!(
74        nsis_fd,
75        "!define MUI_ICON {:?}",
76        fs::canonicalize(&nsis_conf.installer_icon)?
77    )?;
78    writeln!(
79        nsis_fd,
80        "!define MUI_UNICON {:?}",
81        fs::canonicalize(&nsis_conf.uninstaller_icon)?
82    )?;
83
84    if let Some(header_icon) = nsis_conf.installer_header_icon.as_ref() {
85        writeln!(nsis_fd, "!define MUI_HEADERIMAGE")?;
86        writeln!(
87            nsis_fd,
88            "!define MUI_HEADERIMAGE_BITMAP {:?}",
89            fs::canonicalize(header_icon)?
90        )?;
91    }
92    if let Some(installer_sidebar) = nsis_conf.installer_sidebar.as_ref() {
93        writeln!(
94            nsis_fd,
95            "!define MUI_WELCOMEFINISHPAGE_BITMAP {:?}",
96            fs::canonicalize(installer_sidebar)?
97        )?;
98    }
99    if let Some(uninstaller_sidebar) = nsis_conf.uninstaller_sidebar.as_ref() {
100        writeln!(
101            nsis_fd,
102            "!define MUI_UNWELCOMEFINISHPAGE_BITMAP {:?}",
103            fs::canonicalize(uninstaller_sidebar)?
104        )?;
105    }
106
107    Ok(())
108}
109
110fn define_pages(
111    conf: &Config,
112    windows_conf: &WindowsConfig,
113    arch: Arch,
114    nsis_conf: &NsisConfig,
115    nsis_fd: &mut File,
116) -> Result<(), Error> {
117    // Setup pages
118    if nsis_conf.one_click {
119        writeln!(
120            nsis_fd,
121            r#"InstallDir "$LOCALAPPDATA\Programs\{}""#,
122            &conf.metadata.name
123        )?;
124        writeln!(nsis_fd, "RequestExecutionlevel User")?;
125        // Enable silent install.
126        writeln!(nsis_fd, "SilentInstall silent")?;
127    } else {
128        if nsis_conf.per_machine {
129            if arch == Arch::X86_64 {
130                writeln!(
131                    nsis_fd,
132                    r#"InstallDir "$PROGRAMFILES64\{}""#,
133                    &conf.metadata.name
134                )?;
135            } else {
136                writeln!(
137                    nsis_fd,
138                    r#"InstallDir $PROGRAMFILES\{}"#,
139                    &conf.metadata.name
140                )?;
141            }
142            writeln!(nsis_fd, "RequestExecutionlevel Admin")?;
143        } else {
144            writeln!(
145                nsis_fd,
146                r#"InstallDir "$LocalAppData\Programs\{}""#,
147                &conf.metadata.name
148            )?;
149            writeln!(nsis_fd, "RequestExecutionlevel User")?;
150        }
151
152        writeln!(nsis_fd)?;
153
154        if nsis_conf.installer_sidebar.is_some() {
155            writeln!(nsis_fd, "!insertmacro MUI_PAGE_WELCOME")?;
156        }
157
158        if let Some(license_file) = conf.metadata.license_file.as_ref() {
159            writeln!(
160                nsis_fd,
161                "!insertmacro MUI_PAGE_LICENSE {:?}",
162                fs::canonicalize(license_file)?
163            )?;
164        }
165
166        if nsis_conf.allow_to_change_installation_directory {
167            writeln!(nsis_fd, "!insertmacro MUI_PAGE_DIRECTORY")?;
168        }
169
170        writeln!(nsis_fd, "!insertmacro MUI_PAGE_INSTFILES\n")?;
171
172        if nsis_conf.run_after_finish {
173            writeln!(
174                nsis_fd,
175                r#"!define MUI_FINISHPAGE_RUN "$INSTDIR\{}""#,
176                &windows_conf.exe_file
177            )?;
178            writeln!(nsis_fd, "!define MUI_FINISHPAGE_NOREBOOTSUPPORT")?;
179            writeln!(nsis_fd, "!insertmacro MUI_PAGE_FINISH\n")?;
180        }
181
182        if nsis_conf.uninstaller_sidebar.is_some() {
183            writeln!(nsis_fd, "!insertmacro MUI_UNPAGE_WELCOME")?;
184        }
185        writeln!(nsis_fd, "!insertmacro MUI_UNPAGE_CONFIRM")?;
186        writeln!(nsis_fd, "!insertmacro MUI_UNPAGE_INSTFILES")?;
187    }
188
189    Ok(())
190}
191
192fn define_languages(conf: &Config, nsis_fd: &mut File) -> Result<(), Error> {
193    writeln!(nsis_fd)?;
194    writeln!(nsis_fd, r#"!insertmacro MUI_LANGUAGE "English""#)?;
195
196    let build_version = format!("{}.{}", &conf.metadata.version, &conf.metadata.build_id);
197
198    // Version information.
199    writeln!(nsis_fd, r#"VIProductVersion "{}""#, &build_version)?;
200    writeln!(nsis_fd, r#"VIFileVersion "{}""#, &build_version)?;
201
202    writeln!(
203        nsis_fd,
204        r#"VIAddVersionKey /LANG=${{LANG_ENGLISH}} "ProductName" "{}""#,
205        &conf.metadata.product_name
206    )?;
207    writeln!(
208        nsis_fd,
209        r#"VIAddVersionKey /LANG=${{LANG_ENGLISH}} "ProductVersion" "{}""#,
210        &conf.metadata.version
211    )?;
212    writeln!(
213        nsis_fd,
214        r#"VIAddVersionKey /LANG=${{LANG_ENGLISH}} "FileDescription" "{}""#,
215        &conf.metadata.description
216    )?;
217    if let Some(ref company) = conf.metadata.company {
218        writeln!(
219            nsis_fd,
220            r#"VIAddVersionKey /LANG=${{LANG_ENGLISH}} "CompanyName" "{company}""#
221        )?;
222    }
223    if let Some(ref copyright) = conf.metadata.copyright {
224        writeln!(
225            nsis_fd,
226            r#"VIAddVersionKey /LANG=${{LANG_ENGLISH}} "LegalCopyright" "{copyright}""#
227        )?;
228    }
229    writeln!(
230        nsis_fd,
231        r#"VIAddVersionKey /LANG=${{LANG_ENGLISH}} "FileVersion" "{}""#,
232        &build_version
233    )?;
234
235    Ok(())
236}
237
238fn define_uninstall_section(
239    conf: &Config,
240    nsis_conf: &NsisConfig,
241    nsis_fd: &mut File,
242    reg_section: &str,
243    reg_uninst_key: &str,
244) -> Result<(), Error> {
245    // Uninstall section
246    writeln!(nsis_fd, "\nSection \"Uninstall\"")?;
247    writeln!(nsis_fd, r#"  Delete "$INSTDIR\Uninstall.exe""#)?;
248    writeln!(nsis_fd, r#"  RMDir /r "$INSTDIR""#)?;
249    if nsis_conf.run_on_startup {
250        writeln!(
251            nsis_fd,
252            r#"  DeleteRegKey {} "Software\Microsoft\Windows\CurrentVersion\Run\{}""#,
253            reg_section, &conf.metadata.product_name,
254        )?;
255    }
256    writeln!(
257        nsis_fd,
258        r#"  DeleteRegKey {reg_section} "{reg_uninst_key}""#
259    )?;
260    if nsis_conf.create_start_menu_shortcut {
261        writeln!(
262            nsis_fd,
263            r#"  Delete "$SMPROGRAMS\{}.lnk""#,
264            &conf.metadata.product_name
265        )?;
266    }
267    if nsis_conf.create_desktop_shortcut {
268        writeln!(
269            nsis_fd,
270            r#"  Delete "$DESKTOP\{}.lnk""#,
271            &conf.metadata.product_name
272        )?;
273    }
274    writeln!(nsis_fd, "SectionEnd")?;
275    Ok(())
276}
277
278fn define_install_section(
279    conf: &Config,
280    windows_conf: &WindowsConfig,
281    nsis_conf: &NsisConfig,
282    nsis_fd: &mut File,
283    nsis_dir: &Path,
284) -> Result<(), Error> {
285    let files = if let Some(files) = nsis_conf.files.as_ref() {
286        files
287    } else if let Some(files) = windows_conf.files.as_ref() {
288        files
289    } else {
290        return Err(Error::new(
291            ErrorKind::FilesNotSet,
292            "`files` property not set for nsis",
293        ));
294    };
295
296    // Install section
297    writeln!(nsis_fd, "\nSection \"Install\"")?;
298    writeln!(nsis_fd, r#"  SetOutPath "$INSTDIR""#)?;
299    for file in files {
300        file.copy_to(&conf.metadata.src_dir, nsis_dir)?;
301        writeln!(nsis_fd, "  File {}", &file.to)?;
302    }
303    writeln!(nsis_fd, r#"  WriteUninstaller "$INSTDIR\Uninstall.exe""#)?;
304
305    let reg_section = if nsis_conf.per_machine {
306        "HKLM"
307    } else {
308        "HKCU"
309    };
310
311    let reg_uninst_key = format!(
312        r#"Software\Microsoft\Windows\CurrentVersion\Uninstall\{}"#,
313        &conf.metadata.product_name
314    );
315
316    writeln!(
317        nsis_fd,
318        r#"  WriteRegStr {reg_section} "{reg_uninst_key}" "UninstallString" '"$INSTDIR\Uninstall.exe"'"#
319    )?;
320    writeln!(
321        nsis_fd,
322        r#"  WriteRegStr {reg_section} "{reg_uninst_key}" "QuietUninstallString" '"$INSTDIR\Uninstall.exe" /S'"#
323    )?;
324    writeln!(
325        nsis_fd,
326        r#"  WriteRegStr {reg_section} "{reg_uninst_key}" "InstallLocation" "$INSTDIR""#
327    )?;
328    writeln!(
329        nsis_fd,
330        r#"  WriteRegStr {} "{}" "DisplayName" "{}""#,
331        reg_section, reg_uninst_key, &conf.metadata.product_name
332    )?;
333    writeln!(
334        nsis_fd,
335        r#"WriteRegStr {} "{}" "DisplayIcon" "$INSTDIR\{},0""#,
336        reg_section, reg_uninst_key, &windows_conf.exe_file
337    )?;
338    writeln!(
339        nsis_fd,
340        r#"WriteRegStr {} "{}" "DisplayVersion" "{}""#,
341        reg_section, reg_uninst_key, &conf.metadata.version
342    )?;
343    if let Some(company) = conf.metadata.company.as_ref() {
344        writeln!(
345            nsis_fd,
346            r#"WriteRegStr {reg_section} "{reg_uninst_key}" "Publisher" "{company}""#
347        )?;
348    }
349    writeln!(
350        nsis_fd,
351        r#"  WriteRegDWORD {reg_section} "{reg_uninst_key}" "NoModify" "1""#
352    )?;
353    writeln!(
354        nsis_fd,
355        r#"  WriteRegDWORD {reg_section} "{reg_uninst_key}" "NoRepair" "1""#
356    )?;
357    //WriteRegStr HKLM "${REG_UNINST_KEY}" "InstallDate" $1
358
359    if nsis_conf.run_on_startup {
360        writeln!(
361            nsis_fd,
362            r#"  WriteRegStr {} "Software\Microsoft\Windows\CurrentVersion\Run" "{}" '"$INSTDIR\{}"'"#,
363            reg_section, &conf.metadata.product_name, &windows_conf.exe_file
364        )?;
365    }
366    if nsis_conf.create_start_menu_shortcut {
367        writeln!(
368            nsis_fd,
369            r#"  CreateShortcut "$SMPROGRAMS\{}.lnk" "$INSTDIR\{}""#,
370            &conf.metadata.product_name, &windows_conf.exe_file
371        )?;
372    }
373    if nsis_conf.create_desktop_shortcut {
374        writeln!(
375            nsis_fd,
376            r#"  CreateShortcut "$DESKTOP\{}.lnk" "$INSTDIR\{}""#,
377            &conf.metadata.product_name, &windows_conf.exe_file
378        )?;
379    }
380    writeln!(nsis_fd, "SectionEnd")?;
381
382    define_uninstall_section(conf, nsis_conf, nsis_fd, reg_section, &reg_uninst_key)
383}
384
385fn generate_nsis_file(
386    conf: &Config,
387    windows_conf: &WindowsConfig,
388    arch: Arch,
389    nsis_conf: &NsisConfig,
390) -> Result<PathBuf, Error> {
391    let workdir = Path::new(&conf.metadata.workdir);
392    let nsis_dir = workdir.join("nsis");
393    fs::create_dir_all(&nsis_dir)?;
394    let nsis_file = nsis_dir.join("app.nsi");
395    let mut nsis_fd = File::create(&nsis_file)?;
396
397    define_header(conf, arch, nsis_conf, &mut nsis_fd)?;
398    define_icons(nsis_conf, &mut nsis_fd)?;
399    define_pages(conf, windows_conf, arch, nsis_conf, &mut nsis_fd)?;
400    define_languages(conf, &mut nsis_fd)?;
401    define_install_section(conf, windows_conf, nsis_conf, &mut nsis_fd, &nsis_dir)?;
402
403    Ok(nsis_file)
404}
405
406/// Compile nsis script
407fn compile_nsis<P>(nsis_file: &P) -> Result<(), Error>
408where
409    P: AsRef<Path> + Debug,
410{
411    let status = Command::new("makensis").arg(nsis_file.as_ref()).status()?;
412    if status.success() {
413        Ok(())
414    } else {
415        Err(Error::from_string(
416            ErrorKind::NsisCompilerError,
417            format!("`makensis` returns error while compiling {nsis_file:?}"),
418        ))
419    }
420}