본문 바로가기

개발 일지

[Windows] NSIS 를 이용해서 설치 파일 패키징하기

열심히 소프트웨어를 개발해도, 그 프로그램을 다른 사람이 사용할 수 있게 배포하지 않으면, 나 혼자 보고 만족하는 장식장 속의 진열물에 불과하다. 내가 개발한 소프트웨어를 배포할 때에, 사용자가 프로그램을 다운받아서 설치한 후 사용하는 일련의 과정이 복잡하지 않도록 잘 패키징하는 것은, 상용 프로그램에 있어서는 프로그램을 사용하는 유저 경험의 가장 첫 번째 관문인 만큼 굉장히 중요한 과정이라 할 수 있다. Mac 의 경우에는 .dmg 파일이나 .pkg 파일을 이용해서 설치 패키지를 제공하거나 App Store 에 출시할 수 있고, Windows 사용자의 경우에는 인스톨러 파일 (*.exe) 를 다운받아서 설치를 진행하는 과정이 가장 익숙할 것이다. Windows 배포를 위한 방식에는 여러 가지가 있는데,

등 여러 가지 방법을 찾아볼 수 있다. 위의 방법 중 NSIS 를 이용한 설치 파일 패키징에 대해 알아보자.

Nullsoft Scriptable Install System

NSIS 는 윈도우 인스톨러를 제작하기 위한 오픈 소스 시스템이다. Scriptable Install System 이라는 이름이 시사하듯, 설치에 필요한 과정들을 개발자가 직접 설정할 수 있고, 설치를 하는 동안 이루어지는 단계들을 스크립트 파일로 지정할 수 있다. 필요한 경우 굉장히 세세한 customization 도 가능하지만, 기본적으로 제공되는 기능들이 많기 때문에, 간단한 프로그램의 배포 인스톨러 정도는 canonical 한 스크립트로도 처리가 가능한 수준이다. 다음은 NSIS 공식 홈페이지에 올라와 있는 예제 스크립트이다.

; [출처] https://nsis.sourceforge.io/Adding_custom_installer_pages#InstallOptions_Code_Snippet_.231

!include "MUI.nsh"

OutFile "myCustomPage.exe"

Page Custom MyCustomPage MyCustomLeave

Function MyCustomPage
  # If you need to skip the page depending on a condition, call Abort.
  ReserveFile "InstallOptionsFile.ini"
  !insertmacro MUI_INSTALLOPTIONS_EXTRACT "InstallOptionsFile.ini"
  !insertmacro MUI_INSTALLOPTIONS_DISPLAY "InstallOptionsFile.ini"
FunctionEnd

Function MyCustomLeave
  # Form validation here. Call Abort to go back to the page.
  # Use !insertmacro MUI_INSTALLOPTIONS_READ $Var "InstallOptionsFile.ini" ...
  # to get values.
FunctionEnd

Section Dummy
SectionEnd

굉장히 다양하고 복잡한 문법이 있는 것처럼 보이지만, 몇 가지 기본적인 내용만 알기 시작하면, 전반적으로 금방 눈에 익게 된다. NSIS 스크립트에 사용할 수 있는 문법은 공식 문서에 잘 작성되어 있다. 그 중, 예제 스크립트를 작성하면서, 필자가 경험상 자주 필요했던 요소들을 정리해 보았다.

Hello World

가장 기초적인 NSIS 스크립트를 작성하고, 인스톨러를 실행해 보자.

먼저, hello.nsi 파일을 만들고 다음과 같이 작성한다.

Name "Hello World Setup"
OutFile "Hello World Setup.exe"
InstallDir "$PROGRAMFILES\${PRODUCT_NAME}"

Section "Hello World"
    MessageBox MB_OK "Hello World"
SectionEnd

 

그 다음, 공식 홈페이지 에서 NSIS 를 설치한 후, NSIS 를 실행하면, 다음과 같은 화면이 나온다.

NSIS 실행

 

Compile NSI scripts 를 클릭하면, 다음 화면이 나온다.

MakeNSISW 화면

 

화면 위로 hello.nsi 파일을 드래그 앤 드롭 하면, Hello World Setup.exe 파일이 생기는 것을 확인할 수 있다. 파일을 실행시키면 다음과 같이 설치가 진행된다.

Hello World

 

첫 번째 NSIS 설치 파일을 성공적으로 작성했다!!

NSIS 스크립트 작성하기

NSIS 스크립트 정의는 *.nsi 파일에 작성된다. 프로젝트를 생성할 폴더를 만들고, 그 안에 install.nsi 파일을 생성한다. 여기서는 nsis-installer-example 라는 폴더를 구성했다.

mkdir nsis-installer-example
cd nsis-installer-example
type nul >> install.nsi

설치 프로그램의 주 목적은, 개발된 소프트웨어를 사용자의 머신에 복사하여 설치하는 것이 일반적인데, 비슷한 구조를 달성하기 위해서, 몇 가지 텍스트파일을 사용자의 컴퓨터에 설치하는 인스톨러를 패키징하는 것을 목표로 해보자. foo.txt 와 bar.txt 라는 두 텍스트 파일을 준비하자. 편의성을 위해서 dist 라는 폴더 안에 두 텍스트 파일을 준비해 보자.

mkdir dist
echo I have brought peace, freedom, justice, and security to my new empire. > dist/foo.txt
echo Your new empire? > dist/bar.txt

이 두 파일을 사용자의 컴퓨터에 설치하는 인스톨러 스크립트를 작성할 것이다.

기본 설정

먼저, 편의를 위해서 반복해서 사용되는 심볼들을 지정할 수 있다. !define 을 이용하면, 심볼을 정의할 수 있고, 나중에 스크립트에서 사용 가능한다. 예를 들어, 프로그램의 이름과 같은 값은 스크립트 전역에서 사용될 것이기 때문에, 상수로 지정하도록 하겠다.

; Define const
!define PRODUCT_NAME "NsisExample"
!define PRODUCT_VERSION "0.1.0"
!define REG_HKLM_UNINST "Software\Microsoft\Windows\CurrentVersion\Uninstall"

이 예제에서는 프로그램 이름을 NsisExample 로 지정하고 0.1.0 버전이라고 가정하겠다. PRODUCT_NAME 이라는 심볼에 프로그램 이름을, PRODUCT_VERSION 이라는 심볼에 버전 정보를 저장했다. REG_HKLM_UNINST 는 추후에 스크립트에서 사용될 언인스톨러를 위한 레지스트리 키인데, 설치 제거 파일 설정 항목에서 설명하도록 하겠다. 여기서 define 한 심볼들은 스크립트에서 $ 을 붙여서 사용할 수 있다.

다음은 인스톨러의 기본적인 설정들을 할텐데, 설치하는 타겟의 이름, 만들어질 인스톨러 파일의 이름, 그리고 기본 설치 경로 등의 설정을 스크립트에 명시할 수 있다.

Outfile "${PRODUCT_NAME} Setup ${PRODUCT_VERSION}.exe"
Name "${PRODUCT_NAME} ${PRODUCT_VERSION}"

InstallDir "$PROGRAMFILES\${PRODUCT_NAME}"
ShowInstDetails show

Outfile은 만들어지는 설치 파일의 이름을 설정한다. 여기서는 위에서 define 한 심볼들을 이용해서 "NsisExample Setup 0.1.0.exe" 라는 파일이 생성될 것이다.

Name 은 프로그램의 이름을 설정한다. 역시 위에서 define 한 심볼들을 이용해서 "NsisExample 0.1.0" 이라고 이름을 설정했다.

InstallDir 은 기본 설치 경로를 설정한다. $PROGRAMFILES 는 NSIS 스크립트에서 자체적으로 사용할 수 있는 상수들 중 하나인데, 사용할 수 있는 다양한 상수들은 문서에서 확인할 수 있다. 그 중 $PROGRAMFILES는 우리가 윈도우에서 프로그램이 저장되는 경로 (주로 C:\Program Files) 를 가리키는 상수이다.

ShowInstDetails 은 설치 세부 사항을 화면에 표시할 지 여부를 설정한다.

여기서 설정한 부분들 말고도, 공식 문서를 확인하면 다양한 설정들을 확인할 수 있다.

Pages

흔히 볼 수 있는 인스톨러의 실행 장면을 생각해 보면, 먼저 인스톨러를 실행하면 "설치를 시작합니다"와 같은 화면, 그 다음으로 설치 경로 설정, 설치 항목 선택 등의 화면들이 지나가게 된다. 이 각각의 화면을 Page라고 부른다. Page 가 없이 설치 파일을 만들면, 다음과 같이 별도의 유저 상호작용 없이, 바로 설치가 완료된다.

Page 없는 인스톨러

NSIS 의 Page를 이용할 수도 있지만, Modern UI 를 이용해서 페이지를 정의하면, 기본적인 설치 플로우에 대해서는 간편하게 페이지를 구성할 수 있다.

Modern UI의 사용 방법은 MUI2 문서에 잘 작성되어 있다. 먼저 MUI를 사용하기 위해 header 파일을 임포트하고, 페이지에 대한 정보를 명시할 수 있다.

!include "MUI2.nsh"

; Define pages
!define MUI_WELCOMEPAGE_TITLE "Install NSIS Example"
!define MUI_WELCOMEPAGE_TEXT "Example Installer for NSIS"
!insertmacro MUI_PAGE_WELCOME
!insertmacro MUI_PAGE_DIRECTORY
!insertmacro MUI_PAGE_COMPONENTS
!insertmacro MUI_PAGE_INSTFILES
!insertmacro MUI_PAGE_FINISH

!insertmacro MUI_LANGUAGE "Korean"

 

!insertmacro 를 이용해서 어떤 페이지들이 보여지게 될지 설정할 수 있고, !define 으로 되어 있는 부분이 각 페이지에 대한 세부 설정 부분이다. 위에서 정의할 대로, WELCOME, DIRECTORY, COMPONENTS, INSTFILES, FINISH 페이지가 보여질 것이다.

  • MUI_PAGE_WELCOME

환영 페이지이다. 원래 Name 을 이용한 시작 메세지가 표시되지만, MUI_WELCOMEPAGE_TITLE 과 MUI_WELCOMEPAGE_TEXT 를 이용해서 환영 메세지와 제목을 커스터마이즈 했다.

Welcome 페이지

  • MUI_PAGE_DIRECTORY

Windows 사용자들에게는 익숙한, 설치 경로 설정 페이지이다. 여기서 설정한 설치 경로는 $INSTDIR 로 레퍼런스 할 수 있다.

설치 경로 페이지

  • MUI_PAGE_COMPONENTS

어떠한 설치 항목들을 설치할지 선택하게 할 수 있다. 여기서 설치 항목으로 표시되는 부분은 후에 기술할 Section 이란 항목으로 스크립트에서 정의할 수 있다.

설치 항목 선택 페이지

  • MUI_PAGE_INSTFILES

실제로 설치가 이루어지는 페이지이다. 우리가 Section 을 이용해서 정의하는 설치 항목들이 이 페이지에서 실제로 이루어진다.

설치 페이지

  • MUI_PAGE_FINISH

설치가 완료된 후 완료 페이지이다. 이 페이지에서 "Program 실행하기" 와 같은 옵션을 주어서 설치가 끝남과 동시에 프로그램을 실행하도록 설정할 수도 있다.

설치 완료 페이지

인스톨러를 실행시켰을 때에 사용자의 경험을 Page 를 통해 정의했으니, 이제 실제로 설치 과정에서 어떤 일이 일어나는지 설정해보자.

Section

우리가 인스톨러를 실행해서 설치가 진행이 될 때에, 설치가 될 항목들을 정의하는 부분이 Section이다. Section 안에 들어가야 할 명령들로는, 설치 경로에 파일들을 설치하는 것, 레지스트리를 편집하는 것, 시작 메뉴에 바로가기를 만드는 것 등등이 들어갈 수 있다.

Section 이 정의되는 문법은 다음과 같다.

Section [/o] [([!]|[-])section_name] [section_index_output]

SectionEnd

section_name 은 항목의 이름, section_index_output 은 추후에 스크립트에서 section 의 index 를 레퍼런스 할 수 있도록 변수를 정의할 수 있는 부분이다. 위에서 본 것처럼, Section 에서 정의한 설치 과정은 MUI_PAGE_INSTFILES 에서 실제로 실행되는 부분이고, 각각의 Section 은 MUI_PAGE_COMPONENTS 페이지를 넣었다면, 설치 항목으로 선택할 수 있도록 보여질 수 있는데, 이 때 보여지는 방식을 다양한 옵션을 통해 변경할 수 있다.

먼저, "Program" 이라는 이름을 가진 Section 을 만들어보자.

Section "Program" SEC01

SectionEnd

 

이렇게 설정할 경우, MUI_PAGE_COMPONENTS 페이지에서 다음과 같이 보이게 된다.

외로운 Program

Program 이라는 이름의 설치 항목이 보이는 것을 확인할 수 있다. 만약, 유저의 선택을 요하지 않고, 필수적으로 설치되야 하는 항목이라면, section_name 을 정의하지 않거나, section_name 앞에 "-" 가 오도록 작성하면, 설치 항목으로 보이지 않게 설정할 수 있다.

예를 들어, 다음과 같이 Section 을 구성해 보자.

Section "Program" SEC01

SectionEnd

Section "Program Uninstaller"

SectionEnd

 

그러면 다음과 같이 항목에 표시되는 것을 확인할 수 있다.

Program Uninstaller 가 생겼다

 

Program Uninstaller 는 나중에 작성할 프로그램 제거 파일에 대한 Section 인데, 프로그램 제거 파일 같은 경우는 설치할지 말지를 유저에게 표시하지 않고, 항상 설치해야 하는 종류의 항목이니, 페이지에서 숨기는 것이 좋은 선택으로 보인다. 따라서, Section 의 이름을 다음과 같이 바꿔보자.

Section "Program" SEC01

SectionEnd

; Section 의 이름 앞에 - 를 붙임
Section "-Program Uninstaller"

SectionEnd

 

변경 후 인스톨러에는 다음과 같이 보이게 된다.

다시 외로워졌다.

 

공식 문서에 다른 옵션들 역시 찾아볼 수 있는데, Section 앞에 /o 를 붙이면 선택 항목으로 하여 기본값이 미설치로 설정이 되고, section name 앞에 !를 붙이면, 항목 이름을 볼드체로 보이게 할 수 있다. 다음 두 Section 들을 추가해보자.

Section /o "Optional Items"

SectionEnd

Section "!Important Items"

SectionEnd

 

그러면 다음과 같이 보이는 것을 확인할 수 있다.

다양한 옵션을 줄 수 있다

Section Instructions

이제 실제로 설치 시에 어떤 항목들이 실행되어야 하는지 정의하다. Section 안에 정의될 수 있는 Instruction 들도 공식문서에 잘 설명되어 있다. 그 중에서 SetOutPathFile 을 이용해서 "설치경로에 해당 파일들을 복사" 하는 Section 을 만들어 보자.

Section "Program" SEC01
    SetOutPath $INSTDIR
    File "dist\*"
SectionEnd

 

MUI_PAGE_DIRECTORY 에서 설정한 $INSTFILE 을 파일을 작성할 경로로 설정하고, dist 디렉토리 안에 있는 파일들을 해당 위치에 복사하도록 정의했다. 우리는 dist 폴더 안에 foo.txt 와 bar.txt 파일을 넣어 놓았기 때문에, 두 텍스트 파일이 복사될 것이다.

Function

NSIS 스크립트에서도 구문이 길어질 경우, Instruction 들을 Function 으로 묶어서 정의할 수 있다. Function 의 정의는 다음과 같이 할 수 있다.

Function [function_name]
  # some commands
FunctionEnd

 

이렇게 정의된 Function 은 Section 안에서 다음과 같이 호출할 수 있다.

Call [function_name]

 

MessageBox 를 이용하여 메세지를 띄우는 함수를 만들고, 위의 Program Section 에서 호출해 보자.

; Program Section 을 변경
Section "Program" SEC01
    SetOutPath $INSTDIR
    File "dist\*"
    Call InstallAdditionalComponents
SectionEnd

Function InstallAdditionalComponents
    MessageBox MB_OK "Installing additional components"
FunctionEnd

 

InstallAdditionalComponents 이라는 이름의 함수를 만들었고, 그 함수가 Program Section 안에서 실행되게 만들었다. 설치를 해 보면, 설치 중 다음과 같이 메세지박스가 표시된다.

메세지 박스

 

모든 설치가 진행이 완료되고, 설치 경로로 찾아가 보면, 우리가 원했던 대로, 두 텍스트파일이 설치된 것을 확인할 수 있다.

파일이 설치되었다

설치 제거 파일

Windows 에 대해 필자가 OSX 보다 선호하는 몇 안되는 부분 중 하나가 바로 "설치 제거" 기능이다. 제어판 -> 프로그램 제거 에 들어가면, 프로그램을 제거할 수 있는 명확한 UX 가 존재한다. 프로그램 uninstaller 를 작성하고, 그 uninstaller 가 제어판에서 실행될 수 있도록, uninstaller 를 설치하고, 그 정보를 레지스트리에 작성하는 Section 을 구성할 수 있다.

위에서 작성한 "-Program Uninstaller" 라는 Section 안에 uninstaller 를 작성하는 instruction 을 구성하자.

Section "-Program Uninstaller"
    DetailPrint "Register Uninstall Info"
    WriteUninstaller "$INSTDIR\Uninstall.exe"

    WriteRegStr HKLM "${REG_HKLM_UNINST}\${PRODUCT_NAME}" "DisplayName" "${PRODUCT_NAME}"
    WriteRegStr HKLM "${REG_HKLM_UNINST}\${PRODUCT_NAME}" "InstallLocation" "$INSTDIR"
    WriteRegStr HKLM "${REG_HKLM_UNINST}\${PRODUCT_NAME}" "UninstallString" "$INSTDIR\Uninstall.exe"
    MessageBox MB_OK "Install Complete"
SectionEnd

 

이 섹션은 이름 앞에 "-" 이 붙어 있기 때문에, 선택의 여지 없이 항상 실행되는 항목이다. 먼저 $INSTDIR/Uninstall.exe 라는 이름의 설치 제거 파일을 만들도록 하고, WriteRegStr 을 이용해서 레지스트리 키를 편집한다. 이때, REG_HKLM_UNINST 키를 활용하게 되는데, Microsoft 공식 문서 에서 제공하는 uninstall 레직스트리 키인 "HKEY_LOCAL_MACHINE\Software\Microsoft\Windows\CurrentVersion\Uninstall" 을 사용하게 된다. 이 Section 을 추가하면, 다음과 같이 제어판에도 설치 제거 항목이 표시되고, 설치 경로에도 Uninstall.exe 가 생긴다.

제어판에서 제거할 수 있다
Uninstall.exe 가 생겼다

 

이제, Uninstall.exe 를 실행했을 때에 실행해야 할 항목들도 정의해 주자. 먼저, 설치 제거시에도 유저 상호작용이 생기도록, 설치 제거 파일의 페이지들을 MUI 를 이용해서 구성하자.

!insertmacro MUI_UNPAGE_WELCOME
!insertmacro MUI_UNPAGE_CONFIRM
!insertmacro MUI_UNPAGE_INSTFILES
!insertmacro MUI_UNPAGE_FINISH

 

MUI_UNPAGE_INSTFILES 에서 실행될 Section 들도 정의를 해줄 텐데, Section 이름이 "un" 으로 시작되면, NSIS 는 설치 제거 Section 이라고 인식한다.

Section Uninstall
    RMDir /r "$INSTDIR\*"
    RMDir $INSTDIR

    DeleteRegKey HKLM "${REG_HKLM_UNINST}\${PRODUCT_NAME}"
SectionEnd

 

Uninstall 이라는 Section 을 작성했고, 설치 경로에 있는 모든 파일과 설치 경로를 삭제하고, 위에서 편집한 레지스트리 키 역시 삭제하는 instruction 들을 구성했다. 이렇게 제거 파일에서 깔끔하게 설치된 파일 및 레지스트리 키를 제거할 수 있다.

결론

Windows 앱의 패키징을 위해서 NSIS 를 사용하면, 스크립트의 형태로 설치 과정을 정의할 수 있고, 또 Windows 의 시스템의 장점을 살려서 고도화된 유저 경험을 유도할 수 있다. 시작메뉴에 프로그램 추가 혹은 바탕화면 바로가기 만들기 등의 Windows native 한 설치 과정들도 구현할 수 있으니, 커스터마이즈된 설치 패키징을 하기에 적합한 것 같다.

Appendix

전체 스크립트

; Define const
!define PRODUCT_NAME "NsisExample"
!define PRODUCT_VERSION "0.1.0"
!define REG_HKLM_UNINST "Software\Microsoft\Windows\CurrentVersion\Uninstall"

!include "MUI2.nsh"

; Define the name of the installer
Outfile "${PRODUCT_NAME} Setup ${PRODUCT_VERSION}.exe"
Name "${PRODUCT_NAME} ${PRODUCT_VERSION}"
; Install Directory
InstallDir "$PROGRAMFILES\${PRODUCT_NAME}"
ShowInstDetails show

; Define pages
!define MUI_WELCOMEPAGE_TITLE "Install NSIS Example"
!define MUI_WELCOMEPAGE_TEXT "Example Installer for NSIS"
!insertmacro MUI_PAGE_WELCOME
!insertmacro MUI_PAGE_DIRECTORY
!insertmacro MUI_PAGE_COMPONENTS
!insertmacro MUI_PAGE_INSTFILES
!insertmacro MUI_PAGE_FINISH

!insertmacro MUI_UNPAGE_WELCOME
!insertmacro MUI_UNPAGE_CONFIRM
!insertmacro MUI_UNPAGE_INSTFILES
!insertmacro MUI_UNPAGE_FINISH

!insertmacro MUI_LANGUAGE "English"

Section "Program" SEC01
    SetOutPath $INSTDIR
    File "dist\*"
    Call InstallAdditionalComponents
SectionEnd

Section "-Program Uninstaller"
    DetailPrint "Register Uninstall Info"
    WriteUninstaller "$INSTDIR\Uninstall.exe"

    WriteRegStr HKLM "${REG_HKLM_UNINST}\${PRODUCT_NAME}" "DisplayName" "${PRODUCT_NAME}"
    WriteRegStr HKLM "${REG_HKLM_UNINST}\${PRODUCT_NAME}" "InstallLocation" "$INSTDIR"
    WriteRegStr HKLM "${REG_HKLM_UNINST}\${PRODUCT_NAME}" "UninstallString" "$INSTDIR\Uninstall.exe"
    MessageBox MB_OK "Install Complete"
SectionEnd

Section /o "Optional Items"

SectionEnd

Section "!Important Items"

SectionEnd

Section Uninstall
    RMDir /r "$INSTDIR\*"
    RMDir $INSTDIR

    DeleteRegKey HKLM "${REG_HKLM_UNINST}\${PRODUCT_NAME}"
SectionEnd

Function InstallAdditionalComponents
    MessageBox MB_OK "Installing additional components"
FunctionEnd

 

전체 프로젝트는 다음에서 확인 가능합니다.

 

GitHub - k2sebeom/nsis-installer-example: Example nsis script that creates mock installer

Example nsis script that creates mock installer. Contribute to k2sebeom/nsis-installer-example development by creating an account on GitHub.

github.com