Electron Packaging Machine

2woongjae@gmail.com

Mark Lee (이웅재)

  • Studio XID inc. ProtoPie Engineer
  • Seoul.JS 오거나이저
  • 타입스크립트 한국 유저 그룹 오거나이저
  • 일렉트론 한국 유저 그룹 운영진
  • Microsoft MVP - Visual Studio and Development Technologies
  • Code Busking with Jimmy
    • https://www.youtube.com/channel/UCrKE8ihOKHxYHBzI0Ys-Oow

발표 내용

  • 일렉트론으로 만들어진 어플리케이션의 패키징이란 ?
  • 일렉트론 패키징을 도와주는 모든 것들 알아보기
    • electron-packager
    • electron-builder
    • electron-forge
  • 릴리즈 서버 준비 (자동 업데이트 및 설치 파일 서버 자동화)
  • 프로토파이 패키징
  • 패키징과 디플로이 자동화

electron 모듈과 패키징의 기본

'npm i electron -D' 를 하면 ?

Electron.app (macOS)

node_modules/electron/install.js

#!/usr/bin/env node

// maintainer note - x.y.z-ab version in package.json -> x.y.z
var version = require('./package').version.replace(/-.*/, '')

var fs = require('fs')
var os = require('os')
var path = require('path')
var extract = require('extract-zip')
var download = require('electron-download')

var installedVersion = null
try {
  installedVersion = fs.readFileSync(path.join(__dirname, 'dist', 'version'), 'utf-8').replace(/^v/, '')
} catch (ignored) {
  // do nothing
}

var platformPath = getPlatformPath()

if (installedVersion === version && fs.existsSync(path.join(__dirname, platformPath))) {
  process.exit(0)
}

// downloads if not cached
download({
  cache: process.env.electron_config_cache,
  version: version,
  platform: process.env.npm_config_platform,
  arch: process.env.npm_config_arch,
  strictSSL: process.env.npm_config_strict_ssl === 'true',
  force: process.env.force_no_cache === 'true',
  quiet: ['info', 'verbose', 'silly', 'http'].indexOf(process.env.npm_config_loglevel) === -1
}, extractFile)

// unzips and makes path.txt point at the correct executable
function extractFile (err, zipPath) {
  if (err) return onerror(err)
  extract(zipPath, {dir: path.join(__dirname, 'dist')}, function (err) {
    if (err) return onerror(err)
    fs.writeFile(path.join(__dirname, 'path.txt'), platformPath, function (err) {
      if (err) return onerror(err)
    })
  })
}

function onerror (err) {
  throw err
}

function getPlatformPath () {
  var platform = process.env.npm_config_platform || os.platform()

  switch (platform) {
    case 'darwin':
      return 'dist/Electron.app/Contents/MacOS/Electron'
    case 'freebsd':
    case 'linux':
      return 'dist/electron'
    case 'win32':
      return 'dist/electron.exe'
    default:
      throw new Error('Electron builds are not available on platform: ' + platform)
  }
}

electron-userland/electron-download

prebuilt 된 실행 파일의 실행 방법

  • CLI 실행
    • Electron.app/Contents/MacOS/Electron 명령어 실행
  • GUI 에서 실행
    • Electron.app 더블클릭

CLI 에서 실행

GUI 에서 실행

실행 방식

  • 인자로 '실행할 Electron 프로젝트 폴더'를 지정하고 실행
    • '실행할 Electron 프로젝트' 란 package.json 의 main 에 main process 파일이 지정되어 있는 폴더
    • 그 프로젝트를 실행한다.
    • CLI 에서는 뒤에 경로를 붙여서 명령어를 실행한다.
    • GUI 에서는 폴더를 Electron.app 에 끌어다 놓으면 된다.
  • 인자를 지정하지 않고 실행
    • 기본 실행 지정된 Electron Prebuilt 안에 폴더를 실행한다.
      • app.asar 혹은 app 폴더
      • default_app.asar (미리 들어 있음)

실행 환경(OS)에 따라 미리 정해진 폴더

// On macOS
electron/Electron.app/Contents/Resources/app/
├── package.json
├── main.js
└── index.html

// On Windows and Linux
electron/resources/app
├── package.json
├── main.js
└── index.html

MacOS 의 기본 위치

app 폴더 대신 app.asar 파일 가능

// On macOS
electron/Electron.app/Contents/Resources/app.asar
├── package.json
├── main.js
└── index.html

// On Windows and Linux
electron/resources/app.asar
├── package.json
├── main.js
└── index.html

asar 는 왜 권장하는가 ?

  • electron/asar
  • what is asar ?
    • Simple extensive tar-like archive format with indexing
    • Windows node_modules 긴 경로
    • require 속도 향상
    • 코드 보호

Electron 앱 패키징 과정

step1. 각각의 환경에 맞는 prebuilt 된 실행파일 준비

step2. 이 prebuilt 된 실행파일의 실행 방법

  • 1. 단독 실행 => 실행 환경(OS)에 따라 미리 정해진 폴더를 실행
    • 더블 클릭 하거나
    • 커맨드라인에서 인자 없이 실행
  • 2. 인자로 특정 폴더를 지정하여 실행 (개발 모드에서 실행하는 방식)
    • 커맨드라인에서 실행파일 뒤에 폴더를 인자로 하여 실행

실행 환경(OS)에 따라 미리 정해진 폴더

// On macOS
electron/Electron.app/Contents/Resources/app/
├── package.json
├── main.js
└── index.html

// On Windows and Linux
electron/resources/app
├── package.json
├── main.js
└── index.html

step3. 실행파일이 실행하는 폴더

  • ${Project-Folder}/ <= 실행할때 지정된 폴더
    • package.json
      • main 이 지정하는 js 파일을 엔트리 포인트로 사용
  • 그래서 이 실행파일에서 사용하는 node.js 의 버전과 크로미움의 버전이 중요
    • 컴퓨터의 node.js 가 6.x.x 버전이지만,
    • electron 1.7.9 의 node.js 가 7.9.0 이기 때문에
    • async - await 가 사용 가능합니다.
  • 이 프로젝트 폴더가 ' 개발해야할 영역 ' 입니다.

step4. 실행파일의 이름과 아이콘 변경

  • 리브랜딩 이라고 합니다.
  • 일렉트론 프레임워크의 소스코드를 받아서 빌드할때 처리할 수 있지만, 권장하지 않습니다.
  • 미리 빌드된 실행파일로도 이름과 아이콘을 변경할 수 있습니다.
    • 그래서 대부분 미리 빌드된 실행파일을 다운받고,
    • 자신의 프로젝트 소스 폴더만 정해진 자리에 넣은 뒤,
    • 아이콘과 이름만 변경하는 방식을 사용합니다.

step5. 코드 서명

  • 누가 만든 어플리케이션
  • macOS 는 macOS 에서만
  • Windows 는 인증서 사서 => 비싸요

electron-packager

electron-userland/electron-packager

Ready for Packaging

npx electron-packager --icon=icon.icns .

패키징 전후 파일들

// 프로젝트 폴더 (before packaging)
electron-packager-example/
├── node_modules/
├── icon.icns
├── package.json
├── package-lock.json
└── index.js

// 프로젝트 폴더 (after packaging)
electron-packager-example/
├── electron-packager-example-darwin-x64/
├── node_modules/
├── icon.icns
├── package.json
├── package-lock.json
└── index.js

// 패키징 된 app 폴더 (혹은 app.asar 파일)
electron-packager-example-darwin-x64/Contents/Resources/app/
├── icon.icns
├── package.json
├── package-lock.json
└── index.js

electron-builder

electron-userland/electron-builder

electron-userland/electron-builder

electron-builder target

  • macOS
    • dmg
    • pkg
    • mas - 앱스토어
  • Windows
    • nsis
    • nsis-web
    • portable
    • AppX
    • MSI
    • Squirrel.Windows

Don't expect that you can build app for all platforms on one platform.

electron-installer-debian

electron-installer-redhat

electron-installer-flatpak

electron-installer-windows

electron-osx-sign

msi-packager

electron-forge

electron-forge ?

  • electron 앱 개발을 위한 cli 툴
    • angular 에겐 angular-cli
    • react 에겐 create-react-app
    • electron 에겐 electron-forge
  • 설치
    • npm i electron-forge -g
  • 프로젝트 만들기
    • electron-forge init electron-forge-example
  • 스크립트
    • 개발모드 실행 : npm start
    • 패키징 : npm run package
    • 최종 배포 파일 : npm run make
    • 퍼블리싱 : npm run publish

npm i electron-forge -g

~/Project 
➜ npm i electron-forge -g

npm WARN deprecated wrench@1.5.9: wrench.js is deprecated! You should check out fs-extra (https://github.com/jprichardson/node-fs-extra) for any operations you were using wrench for. Thanks for all the usage over the years.
npm WARN deprecated minimatch@0.3.0: Please update to minimatch 3.0.2 or higher to avoid a RegExp DoS issue
/Users/mark/.nvm/versions/node/v8.9.0/bin/electron-forge-vscode-win -> /Users/mark/.nvm/versions/node/v8.9.0/lib/node_modules/electron-forge/script/vscode.cmd
/Users/mark/.nvm/versions/node/v8.9.0/bin/forge -> /Users/mark/.nvm/versions/node/v8.9.0/lib/node_modules/electron-forge/dist/electron-forge.js
/Users/mark/.nvm/versions/node/v8.9.0/bin/electron-forge-vscode-nix -> /Users/mark/.nvm/versions/node/v8.9.0/lib/node_modules/electron-forge/script/vscode.sh
/Users/mark/.nvm/versions/node/v8.9.0/bin/electron-forge -> /Users/mark/.nvm/versions/node/v8.9.0/lib/node_modules/electron-forge/dist/electron-forge.js

> macos-alias@0.2.11 install /Users/mark/.nvm/versions/node/v8.9.0/lib/node_modules/electron-forge/node_modules/macos-alias
> node-gyp rebuild

  CXX(target) Release/obj.target/volume/src/volume.o
  SOLINK_MODULE(target) Release/volume.node

> fs-xattr@0.1.17 install /Users/mark/.nvm/versions/node/v8.9.0/lib/node_modules/electron-forge/node_modules/fs-xattr
> node-gyp rebuild

  CXX(target) Release/obj.target/xattr/src/async.o
  CXX(target) Release/obj.target/xattr/src/error.o
  CXX(target) Release/obj.target/xattr/src/sync.o
  CXX(target) Release/obj.target/xattr/src/util.o
  CXX(target) Release/obj.target/xattr/src/xattr.o
  SOLINK_MODULE(target) Release/xattr.node

> electron-forge@4.1.2 install /Users/mark/.nvm/versions/node/v8.9.0/lib/node_modules/electron-forge
> node tabtab-install.js


[tabtab] Adding source line to load /Users/mark/.nvm/versions/node/v8.9.0/lib/node_modules/electron-forge/node_modules/tabtab/.completions/electron-forge.zsh
in /Users/mark/.zshrc


> spawn-sync@1.0.15 postinstall /Users/mark/.nvm/versions/node/v8.9.0/lib/node_modules/electron-forge/node_modules/spawn-sync
> node postinstall

+ electron-forge@4.1.2
added 596 packages in 112.037s

electron-forge init electron-forge-example

~/Project took 1m 54s 
➜ electron-forge init electron-forge-example
✔ Checking your system
✔ Initializing Project Directory
✔ Initializing Git Repository
✔ Copying Starter Files
✔ Initializing NPM Module
✔ Installing NPM Dependencies

npm start

~/Project/electron-forge-example on  master [?] is 📦 v1.0.0 via ⬢ v8.9.0 
➜ npm start

> electron-forge-example@1.0.0 start /Users/mark/Project/electron-forge-example
> electron-forge start

✔ Checking your system
✔ Locating Application
✔ Preparing native dependencies
✔ Launching Application
Warning, the following targets are using a decimal version:

  electron: 1.7

We recommend using a string for minor/patch versions to avoid numbers like 6.10
getting parsed as 6.1, which can lead to unexpected behavior.

npm run package - electron-packager

~/Project/electron-forge-example on  master [?] is 📦 v1.0.0 via ⬢ v8.9.0 
➜ npm run package

> electron-forge-example@1.0.0 package /Users/mark/Project/electron-forge-example
> electron-forge package

✔ Checking your system
✔ Preparing to Package Application for arch: x64
✔ Compiling Application
✔ Preparing native dependencies
✔ Packaging Application

npm run make

~/Project/electron-forge-example on  master [?] is 📦 v1.0.0 via ⬢ v8.9.0 took 37s 
➜ npm run make              

> electron-forge-example@1.0.0 make /Users/mark/Project/electron-forge-example
> electron-forge make

✔ Checking your system
✔ Resolving Forge Config
We need to package your application before we can make it
✔ Preparing to Package Application for arch: x64
✔ Compiling Application
✔ Preparing native dependencies
✔ Packaging Application
Making for the following targets:
✔ Making for target: zip - On platform: darwin - For arch: x64
✔ Making for target: dmg - On platform: darwin - For arch: x64

package.json

electron-forge make

npm run publish

publish : 배포용 파일 업로드

step4. '설치 파일'로 만들기

  • 토이 프로젝트나, 내부용 앱이라면 step3 에서 멈춰도 충분하지만,
  • 진정한 상용 앱이라면 .app / .zip 으로 줄순 없자나요?
    • macOS => .dmg 로 만들기
    • Windows => .exe 로 만들기 (NOT 실행 프로그램)
  • 이것도 만들어서 다 할수 있지만,
    • 우리에겐 'electron-builder' 가 있습니다.
    • 'electron-packager' 는 step3 까지만 해줍니다.
  • 그리고 설치 파일로 만들기 전, 후에는 codesign 이 필요합니다.

step5. 배포용 서버에 설치파일 올리기

자동 업데이트 ??

autoUpdater 모듈 - Squirrel interface

autoUpdater 기본

// 업데이트 서버 주소 설정 및 초기화
autoUpdater.setFeedURL(`http://127.0.0.1:${port}/update/latest`);

// 업데이트 있는지 체크            
autoUpdater.checkForUpdates();
            
// 업데이트 파일이 다운로드 다 되었는지 확인하는 이벤트 바인딩
autoUpdater.on('update-downloaded', async() => {

    // 앱 종료 후 업데이트 후 재시작
    autoUpdater.quitAndInstall();
            
});

macOS 에서의 Squirrel

  • 사용이 생각보다 어렵지 않습니다.
    • 업데이트용 파일인 .zip 파일을 electron-builder 에서 지원
  • 설치 및 실행 파일에 별도의 추가 변경이 필요하지 않습니다.
    • Windows 는 실행 파일 구조 자체가 변합니다.
    • macOS 는 Squirrel.mac 이 built-in 되어 있습니다.
    • codesign 이 꼭 되어있어야 합니다.
  • 단점
    • 다운로드 진행 상태를 알수 없습니다.
      • 파일이 60MB 이상인걸?
      • 역대급 꼼수
    • 무조건 업데이트 파일 다운로드는 처음부터
    • 릴리즈노트는 다운로드 끝나고?

macOS 에서의 Squirrel

일단 자동 업데이트 서버부터!

  • electron-release-server 라는 프로젝트를 사용​
    • https://github.com/ArekSredzki/electron-release-server
    • squirrel 지원
    • 다양한 다운로드 링크 제공
    • 소스만 받아서 서버에 설치 후 쉽게 사용 가능
    • sails.js 사용 - 전 잘 몰라요
    • DB 는 postgresql (RDS)

macOS 최종 절차

  • step1. 앱 최초 실행시 url 룰에 맞게 릴리즈 서버를 호출
    • 204 면 업데이트 없음.
    • 아니면 (200), 있으니깐 이때 response 에 넣어둔 릴리즈 노트를 받아 사용 
  • step2. 릴리즈 노트를 보여주며, 업데이트 다운로드 버튼을 누르도록
    • 캔슬할수도...
    • 다운로드 버튼을 누르면
  • step3. 로컬에 squirrel 용 .zip 파일을 다운로드
    • 프로그레스 가능
    • 이미 받아둔거 있으면 (체크섬), 다시 사용안하도록 처리 가능
  • step4. 로컬에 짱 박아둔 웹서버를 이용하여, autoUpdater 모듈 사용
    • autoUpdater.setFeedURL(`http://127.0.0.1:${port}/update/latest`);

Squirrel.Windows

  • macOS 와는 다릅니다.
    • 클라이언트에서도 다릅니다.
      • 통으로 새로운 파일을 받아서 갈아끼는 스타일이 아닙니다.
      • 확장자도 .zip 아니고, .nupkg 입니다.
      • 이전 버전과의 diff 를 만들어서 업데이트 처리합니다.
      • 업데이트 설치시 gif 파일이 나타납니다.
      • 설치를 아무데나 할수가 없습니다.
      • 빌드시 이전 버전을 기준삼아 빌드해서 diff 파일을 만듭니다.
    • 서버에서도 다릅니다.
      • 응답이 파일로 옵니다. (헐)
http://download.myapp.com/update/win32/:version/RELEASES
http://download.myapp.com/update/win64/:version/RELEASES
http://download.myapp.com/update/win32/:version/:channel/RELEASES
http://download.myapp.com/update/win64/:version/:channel/RELEASES

Squirrel.Windows 포기

  • VSCode 의 영향이 컸습니다.
    • 업데이트를 누르면 재설치 파일이 실행됩니다.
    • inno-setup 을 사용합니다.
      • 사실 nsis, squirrel 의 문제점을 알고, 대안을 모색하다 발견
        • https://blogs.msdn.microsoft.com/vscode/2015/09/11/updated-windows-installer-for-vs-code/
      • 역시 난 틀리지 않았어를 외치며, 한동안 어깨뽕 들어감.
      • 개발 회의를 통해 설명하고, 깔끔하게 inno-setup 으로 전환
      • VSCode 는 electron 계의 표준전과

Updated Windows Installer For VS Code

Windows 최종 절차 (계획안) - macOS 따라쟁이

  • step1. 앱 최초 실행시 url 룰에 맞게 릴리즈 서버를 호출
    • 204 면 업데이트 없음.
    • 아니면 (200), 있으니깐 이때 response 에 넣어둔 릴리즈 노트를 받아 사용 
  • step2. 릴리즈 노트를 보여주며, 업데이트 다운로드 버튼을 누르도록
    • 캔슬할수도...
    • 다운로드 버튼을 누르면
  • step3. 로컬에 inno-setup 으로 빌드된 .exe 파일을 다운로드
    • 프로그레스 가능
    • 이미 받아둔거 있으면(체크섬), 다시 사용안하도록 처리 가능
  • step4. 프로그램을 종료시키면서 Setup.exe 파일을 실행
    • shell.openItem(path);

inno setup 으로 설치파일 만들기

npm run pack-prod

  • step1. 앱 실행파일 만들기
    • electron-builder
    • package.json 에 build.win.target: [] 로 설정하고, 아이콘만...
    • 그러면 dist/win-ia32-unpacked 폴더가 생기고, 실행파일이 생김
  • step2. 실행 폴더 안의 모든 파일을 codesign 처리
    • 인증서 처리 스폐셜 땡쓰 투 Tom Kim@Drama & Company
  • step3. inno setup 을 통해 -Setup.exe 파일 생성
  • step4. 그 설치 파일을 codesign

npm run pack-prod

inno setup

  • http://www.jrsoftware.org
  • free installer
  • 설치 파일 생성 마법사도 있음
  • npm 모듈도 있음
    • innosetup-compiler
    • https://www.npmjs.com/package/innosetup-compiler
  • 스크립트로 정의할수 있는데 스크립트는 Delphi
    • 델파이 모르면 베끼기
    • https://github.com/Microsoft/vscode/blob/master/build/win32/code.iss
  • 레지스트리 제어 (.pie 확장자 아이콘 처리)
  • 설치시 추가 기능 제공
  • 삭제시 할일 처리

delphi script

; 프로그램 전체에 대한 설정을 정의한다.
[Setup]
AppId={#AppId}
AppName={#NameLong}
AppVerName={#NameVersion}
AppPublisher=Studio XID, Inc.
AppPublisherURL={#Homepage}
AppSupportURL={#Homepage}docs/
;AppUpdatesURL={#Homepage}
DefaultDirName={pf}\{#DirName}
DefaultGroupName={#NameLong}
AllowNoIcons=yes
OutputDir={#RepoDir}\dist
OutputBaseFilename=ProtoPie-{#Version}-Setup
Compression=lzma
SolidCompression=yes
AppMutex={#AppMutex}
WizardImageFile={#RepoDir}\build\win32\inno_big.bmp
WizardSmallImageFile={#RepoDir}\build\win32\inno_small.bmp
SetupIconFile={#RepoDir}\build\win32\icon_setup.ico
UninstallDisplayIcon={app}\{#ExeBasename}.exe
ChangesEnvironment=true
ChangesAssociations=true
MinVersion=6.1.7600
SourceDir={#SourceDir}
AppVersion={#Version}
VersionInfoVersion={#RawVersion}
ShowLanguageDialog=auto
CloseApplications=force
; PrivilegesRequired=admin

; 프로그램이 사용하는 언어를 정의한다.
[Languages]
Name: "english"; MessagesFile: "compiler:Default.isl,{#RepoDir}\build\win32\i18n\messages.en.isl"
Name: "simplifiedChinese"; MessagesFile: "{#RepoDir}\build\win32\i18n\Default.zh-cn.isl,{#RepoDir}\build\win32\i18n\messages.zh-cn.isl"

; 설치시에 제거 할 파일이 있으면 정의한다.
[InstallDelete]
Type: filesandordirs; Name: {app}\resources

; 설치 프로세스를 사용자 측에서 상세하게 설정하는 경우에 각각의 처리(작업)에 대해 정의한다.
[Tasks]
Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"
Name: "quicklaunchicon"; Description: "{cm:CreateQuickLaunchIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked; OnlyBelowVersion: 0,6.1
Name: "associatewithfiles"; Description: "{cm:AssociateWithFiles,{#NameShort}}"; GroupDescription: "{cm:Other}"
Name: "runcode"; Description: "{cm:RunAfter,{#NameShort}}"; GroupDescription: "{cm:Other}"; Check: WizardSilent

...

업데이트 서버 제작 계획안

// make `ProtoPie-${Version}-Setup.exe` file
npm run pack-prod

// make `${Version}.json` file
// upload to S3 - `${Version.json}` file, `ProtoPie-${Version}-Setup.exe` file
npm run deploy

[macOS] response 마이그레이션

{
    "url": "http://release.protopie.io/ProtoPie-3.2.3-mac.zip",
    "name": "3.2.3",
    "notes": "## 3.2.3\n### 3.2.3\n- <strong>Special thanks to Jaret(郑皓), 田中良樹, and Shue(尹舒)</strong> (in alphabetical order)\n- Fixed the problem to lose images under a certain procedure\n- Improved the stability of the application\n- Added ProtoPie improvement program\nmetadata:{\"osx\":{\"size\":75391013,\"checksum\":\"39432d1cb5b4214515a443d4bec719e9d2b787ab\"}}\n## 3.2.2\n### 3.2.2\n- (3.2.2) Fixed window control bug\n- (3.2.1) Fixed dialog bug\n- Replace image layers\n- Copy and paste scenes across files\n- Open recent files\n- Rename Interaction Pieces' names\n- Keyboard shortcuts for panning and zooming canvas panel\n- Spinner interface to adjust values on the property panel\n- Rebuilt Sketch Import feature\n- Enhanced Preview's performance and interactions\n- Improved usability and fixed minor bugs\nmetadata:{\"osx\":{\"size\":72656810,\"checksum\":\"4c45300147ac566e6cb5975cd7c637da0a3d6fcd\"}}",
    "metadata": {
        "osx": {
            "size": 75391013,
            "checksum": "39432d1cb5b4214515a443d4bec719e9d2b787ab"
        }
    },
    "pub_date": "2017-03-21T09:54:30.939Z"
}

[macOS] {Version}.json

{
    "version": "3.2.3",
    "note": "### 3.2.3\n- <strong>Special thanks to Jaret(郑皓), 田中良樹, and Shue(尹舒)</strong> (in alphabetical order)\n- Fixed the problem to lose images under a certain procedure\n- Improved the stability of the application\n- Added ProtoPie improvement program",
    "file": "ProtoPie-3.2.3-mac.zip",
    "metadata": {
        "osx": {
            "size": 75391013,
            "checksum": "39432d1cb5b4214515a443d4bec719e9d2b787ab"
        }
    },
    "pub_date": "2017-03-21T09:54:30.939Z",
    "state": "enabled"
}

[Windows] {Version}.json

{
    "version": "3.2.3",
    "note": "### 3.2.3\n- <strong>Official version for Windows</strong>\n- Thank you for your effort to improve ProtoPie for Windows.\n- Beta test program is over on March 31, 2017 in KST.\n- Please jump in and ride on the official version. ;)",
    "file": "ProtoPie-3.2.3-Setup.exe",
    "metadata": {
        "win32": {
            "size": 55642520,
            "checksum": "5df925f649af2924e48bbd935eb9a3f4807bad65"
        }
    },
    "pub_date": "2017-03-31T05:55:56.133Z",
    "state": "enabled"
}

S3

버전 처리

  • 다양한 버전의 우선순위
    • v3.3.0
    • v3.3.0-qa.1
    • v3.3.0-winrc.1
    • v3.3.0-rc.1
  • http://semver.org/ => Semantic Versioning
  • https://www.npmjs.com/package/semver
import * as semver from 'semver';

semver.gt(version, '3.3.0');

semver.lte(version, '3.1.0')

ec2 - express

// 라우팅 핸들러 설정
router.get('/:platform/:version/', ctrl.index);

// 라우팅 함수
async function index(req, res) {
    // 플랫폼과 버전을 받음
    const platform: string = req.params.platform;
    const version: string = req.params.version;

    if (platform !== 'darwin_x64' && platform !== 'win32') {
        return res.status(400).end();
    }

    if (semver.valid(version) === null) {
        return res.status(400).end();
    }

    // 버전 관리자를 통해 S3 로 부터 최신 버전 리스트를 업데이트
    const versionManager: IVersionManager = VersionManager.getInstance();    
    await versionManager.update();
    // 최신 버전 리스트에서 요청받은 플랫폼과 버전레 맞춰 response 에 담을 내용을 만들어 냄.
    const result = versionManager.getResult(platform, version);

    if (result) {
        res.json(result);
    } else {
        res.sendStatus(204);
    }
}

VersionManager.ts

export class VersionManager implements IVersionManager {
    // 매니저는 단일 객체

    public getResult(platfrom: string, version: string) {
        const platfromId: string = (platfrom === 'darwin_x64') ? 'darwin' : platfrom;
        const versionInPlatform: IVersion[] = this._versionList.filter(v => {
            if ((v.getPlatform() === platfromId) && semver.gt(v.getVersion(), version)) {
                if (MODE === 'production' || MODE === 'qa') {
                    return v.getState() === 'enabled';
                } else {
                    return v.getState() !== 'disabled';
                }
            } else {
                return false;
            }
        });
        versionInPlatform.sort((l, r) => semver.gt(l.getVersion(), r.getVersion()) ? -1 : 1);
        
        if (versionInPlatform.length > 0) {
            const url = versionInPlatform[0].getFile();
            const name = versionInPlatform[0].getVersion();
            const notes = this._getNotes(versionInPlatform);
            const metadata = versionInPlatform[0].getMetadata();
            const pub_date = versionInPlatform[0].getDate();

            return {
                url,
                name,
                notes,
                metadata,
                pub_date
            };
        } else {
            return null;
        }
    }

    public async update(): Promise<void> {
        let s3List = null;
        try {
            s3List = await getS3List();
        } catch (e) {
            await sendSNS('Autoupdate Server : getS3List 실패', '자동업데이트 서버에서 S3 로 부터 JSON 리스트를 읽어오다가 실패함.');
            return;
        }

        let s3Objects = null;
        try {
            s3Objects = await getS3Objects(s3List);
        } catch (e) {
            await sendSNS('Autoupdate Server : getS3Objects 실패', '자동업데이트 서버에서 S3 로 부터 JSON 파일을 읽어오다가 실패함.');
            return;
        }

        this._setVersions(s3List, s3Objects);
    }

    private _setVersions(s3List: any[], s3Objects: any[]): void {
        this._versionList = [];
        s3List.forEach((item, index) => {
           this._versionList.push(new Version(s3List[index], s3Objects[index]));
        });
    }

    private _getNotes(versionInPlatform): string {
        const notes = [];
        versionInPlatform.forEach((item, index) => {
            if (index < 3) {
                notes.push(`## ${item.getVersion()}\n${item.getNote()}\nmetadata:${JSON.stringify(item.getMetadata())}`);
            }
        });
        return notes.join('\n');
    }
}

Staging Server

ProtoPie packaging & deploy

1. npm run clean

console.log('Cleaning up Output and intermediate directory...');

exec('npm run clean');

2. npm run transpile

console.log('Transpile source codes..');

exec('npm run transpile');

3. npm run webpack

if (flavor === 'production') {

    console.log('Packing client codes.. in Release');
    
    exec('npm run webpack-prod');

} else {
    
    console.log('Packing client codes..');
    
    exec('npm run webpack');

}

4. make directory

console.log('Creating intermediate directory...');
mkdir('app');

console.log('Creating distribution specified files...');
cat_write(JSON.stringify({serverMode:ENV}), 'output/server_env.json');
cat_write(JSON.stringify({flavor}), 'output/buildflavor.json');
cp('output/buildflavor/' + flavor + '.js', 'output/buildflavor/current.tmp');
rm_rf('output/buildflavor/*.js');
cp('output/buildflavor/current.tmp', 'output/buildflavor/current.js');

mkdir('bin');

5. npm run pkg

console.log('Packing executable binary file with zeit/pkg...');

if (process.platform === 'win32') {

    exec('npm run pkg-win32');
    exec('npm run pkg-win64');

} else {

    exec('npm run pkg');

}

6. move folder

console.log('Creating distribution specified package.json file...');

cat_write(create_production_packages_json(), 'app/package.json');

console.log('Creating resources to be packed in distribution...');

cp('output', 'app/output');
cp('locale', 'app/locale');
cp('extbin', 'app/extbin');
cp('static', 'app/static');

rm_rf('app/static/index.js');

mv('app/static/index_production.js', 'app/static/index.js');

if (process.platform === 'win32') {

    cp('resources', 'app/resources');

}

7. remove unused soruce

// 배포본 생성에 포함하지 않을 코드 제거 (enclose 컴파일 했으므로 browser쪽은 불필요)
rm_rf('./app/output/browser');
rm_rf('./app/output/browser-test');
rm_rf('./app/output/common-test');
rm_rf('./app/output/testlib-test');
rm_rf('./app/output/client-test');
rm_rf('./app/output/testlib');

// Removing source map files
for(let mapFile of find('./app/output', /.*\.js\.map$/)) {

    rm_rf(mapFile);

}

7. npm i

if (!isDraftBuild) {

    console.log('Downloading distribution dependencies...');
    cd('app');
    exec('npm install');
    cd(PROJECT_ROOT);

} else {

    console.log('Copying development dependencies... (DRAFT MODE: Fast but bigger dependencies)');
    cp('node_modules', 'app/node_modules');

}

8. electron-builder & inno-setup

if (process.platform === 'win32') {
    exec(`node_modules\\.bin\\build.cmd --ia32`);
    mv('dist/win-ia32-unpacked/resources/bin/server32.exe', 'dist/win-ia32-unpacked/resources/bin/server.exe');
    rm_rf('dist/win-ia32-unpacked/resources/bin/server64.exe');
    rm_rf('dist/win-ia32-unpacked/resources/extbin/recorder/ffmpeg-win64.exe');
    rm_rf('dist/win-ia32-unpacked/resources/extbin/recorder/ffmpeg');

    exec(`node_modules\\.bin\\build.cmd --x64`);
    mv('dist/win-unpacked/resources/bin/server64.exe', 'dist/win-unpacked/resources/bin/server.exe');
    rm_rf('dist/win-unpacked/resources/bin/server32.exe');
    rm_rf('dist/win-unpacked/resources/extbin/recorder/ffmpeg-win32.exe');
    rm_rf('dist/win-unpacked/resources/extbin/recorder/ffmpeg');

    rm_rf('./dist/win-ia32-unpacked/resources/app-update.yml');
    rm_rf('./dist/win-unpacked/resources/app-update.yml');


    console.log('Code signing electron files...');

    sign_files(path.join(__dirname, '../dist/win-ia32-unpacked'));
    sign_files(path.join(__dirname, '../dist/win-ia32-unpacked/resources/bin'));
    sign_files(path.join(__dirname, '../dist/win-ia32-unpacked/resources/extbin'));

    sign_files(path.join(__dirname, '../dist/win-unpacked'));
    sign_files(path.join(__dirname, '../dist/win-unpacked/resources/bin'));
    sign_files(path.join(__dirname, '../dist/win-unpacked/resources/extbin'));

    console.log('InnoSetup packaging...');
    const issPath = path.join(__dirname, '../build/win32/code.iss');

    packageInnoSetup(issPath, getInnoSetupDefinition('ia32'), () => {
        console.log('Pack finished for ia32 installer binary');

        packageInnoSetup(issPath, getInnoSetupDefinition('x64'), () => {
            console.log('Pack finished for x64 installer binary');
            sign_files(path.join(__dirname, '../dist'));

            cleanup();
        });
    });
} else {
    rm_rf('extbin/recorder/ffmpeg-win32.exe');
    rm_rf('extbin/recorder/ffmpeg-win64.exe');
    cp('bin', 'app/bin');
    exec('./node_modules/.bin/build');
    rm_rf('./dist/latest-mac.json');
    cp('ffmpeg/ffmpeg-win32.exe', 'extbin/recorder/ffmpeg-win32.exe');
    cp('ffmpeg/ffmpeg-win64.exe', 'extbin/recorder/ffmpeg-win64.exe');
    cleanup();
}

Automation

Why ??

  • Daily Build
    • develop 브랜치를 매일 최종 배포파일로 만들어, 내부적으로 공유
    • 바로바로 치명적인 상태를 체크할 수 있음.
  • 엔지니어가 macOS 와 Windows 에서 각각 빌드하기 귀찮음힘듦
  • 깃랩 CI 에서 각각의 플랫폼에 맞는 빌드 및 테스트가 자주 문제가 생김
    • 문제는 해결하면 되지만, 매번 귀찮음
  • 무심코 던진 말에 제가 심심할때 하는 걸로...

Plan ??

  • macOS 와 Windows 머신을 준비
    • Mac mini
    • NUC
  • Electron App
    • macOS 와 Windows 에서 쉽게 사용 가능한 앱을 만들 수 있음.
    • 평소 Tray 모드
    • 설정을 위한 창을 열수 있도록
  • 어떤 이벤트에 반응 ??
    • 자동
      • Git
      • 타이머
    • Web Browser - Admin
      • 클릭

Settings (Concept)

Process

Slack

JS 개발자라면, 내년을 기대해주세요!

https://seoul.js.org/

Electron 패키징 머신 구축기

By Woongjae Lee

Electron 패키징 머신 구축기

play.node 2017 발표 자료

  • 2,300