2017-10-19 23:04:47 +00:00
/* eslint-disable no-sync */
2017-10-14 13:45:21 +00:00
/// <reference path="./globals.d.ts" />
2016-05-16 19:00:56 +00:00
2017-07-28 12:25:05 +00:00
import * as path from 'path'
import * as cp from 'child_process'
import * as fs from 'fs-extra'
2020-07-08 08:41:20 +00:00
import packager , {
2020-11-28 14:33:13 +00:00
OfficialArch ,
OsxNotarizeOptions ,
OsxSignOptions ,
2020-07-08 08:41:20 +00:00
Options ,
} from 'electron-packager'
2020-07-14 12:55:13 +00:00
import frontMatter from 'front-matter'
2018-01-17 12:26:55 +00:00
import { externals } from '../app/webpack.common'
2018-03-29 05:43:30 +00:00
interface IChooseALicense {
readonly title : string
readonly nickname? : string
readonly featured? : boolean
readonly hidden? : boolean
}
export interface ILicense {
readonly name : string
readonly featured : boolean
readonly body : string
readonly hidden : boolean
}
2017-09-24 00:01:43 +00:00
import {
getBundleID ,
getCompanyName ,
getProductName ,
2017-11-20 01:30:16 +00:00
} from '../app/package-info'
2019-11-04 18:41:06 +00:00
import {
getChannel ,
getDistRoot ,
getExecutableName ,
isPublishable ,
2019-11-20 17:44:01 +00:00
getIconFileName ,
2019-11-04 18:41:06 +00:00
} from './dist-info'
2020-08-25 12:38:18 +00:00
import { isCircleCI , isGitHubActions } from './build-platforms'
2019-01-14 20:43:33 +00:00
2019-01-14 21:34:47 +00:00
import { updateLicenseDump } from './licenses/update-license-dump'
2019-01-14 20:43:33 +00:00
import { verifyInjectedSassVariables } from './validate-sass/validate-all'
2016-05-16 19:00:56 +00:00
const projectRoot = path . join ( __dirname , '..' )
2019-11-03 22:01:44 +00:00
const entitlementsPath = ` ${ projectRoot } /script/entitlements.plist `
2019-11-03 23:09:59 +00:00
const extendInfoPath = ` ${ projectRoot } /script/info.plist `
2016-07-21 18:31:30 +00:00
const outRoot = path . join ( projectRoot , 'out' )
2016-05-16 19:00:56 +00:00
2019-11-04 18:41:06 +00:00
const isPublishableBuild = isPublishable ( )
2019-11-05 18:01:34 +00:00
const isDevelopmentBuild = getChannel ( ) === 'development'
2017-04-05 19:33:31 +00:00
2019-11-04 18:41:06 +00:00
console . log ( ` Building for ${ getChannel ( ) } … ` )
2016-05-24 19:20:23 +00:00
2016-05-24 14:56:56 +00:00
console . log ( 'Removing old distribution…' )
2017-09-24 00:01:43 +00:00
fs . removeSync ( getDistRoot ( ) )
2016-05-23 19:01:29 +00:00
2016-07-21 18:31:30 +00:00
console . log ( 'Copying dependencies…' )
copyDependencies ( )
2016-05-24 14:56:56 +00:00
2016-09-02 20:45:19 +00:00
console . log ( 'Packaging emoji…' )
copyEmoji ( )
2016-11-02 15:01:19 +00:00
console . log ( 'Copying static resources…' )
copyStaticResources ( )
2016-09-15 19:36:21 +00:00
2018-03-29 05:43:30 +00:00
console . log ( 'Parsing license metadata…' )
2018-04-03 04:47:22 +00:00
generateLicenseMetadata ( outRoot )
2018-03-29 05:43:30 +00:00
2018-03-29 04:31:45 +00:00
moveAnalysisFiles ( )
2020-08-25 12:51:11 +00:00
if ( isGitHubActions ( ) && process . platform === 'darwin' && isPublishableBuild ) {
2016-07-21 18:31:30 +00:00
console . log ( 'Setting up keychain…' )
cp . execSync ( path . join ( __dirname , 'setup-macos-keychain' ) )
}
2019-01-14 20:43:33 +00:00
verifyInjectedSassVariables ( outRoot )
2019-01-14 21:34:47 +00:00
. catch ( err = > {
2017-07-13 14:21:09 +00:00
console . error (
2019-01-14 20:43:33 +00:00
'Error verifying the Sass variables in the rendered app. This is fatal for a published build.'
2017-07-13 14:21:09 +00:00
)
2016-05-16 19:00:56 +00:00
2019-11-05 18:01:34 +00:00
if ( ! isDevelopmentBuild ) {
2017-04-06 02:39:33 +00:00
process . exit ( 1 )
}
2019-01-14 21:34:47 +00:00
} )
2019-01-14 20:43:33 +00:00
. then ( ( ) = > {
console . log ( 'Updating our licenses dump…' )
return updateLicenseDump ( projectRoot , outRoot ) . catch ( err = > {
console . error (
'Error updating the license dump. This is fatal for a published build.'
)
console . error ( err )
2019-11-05 18:01:34 +00:00
if ( ! isDevelopmentBuild ) {
2019-01-14 20:43:33 +00:00
process . exit ( 1 )
}
} )
} )
2019-01-14 21:34:47 +00:00
. then ( ( ) = > {
console . log ( 'Packaging…' )
return packageApp ( )
} )
. catch ( err = > {
2018-07-16 19:52:33 +00:00
console . error ( err )
process . exit ( 1 )
2019-01-14 21:34:47 +00:00
} )
. then ( appPaths = > {
console . log ( ` Built to ${ appPaths } ` )
} )
2016-05-24 18:48:10 +00:00
2017-08-09 20:44:41 +00:00
/ * *
* The additional packager options not included in the existing typing .
*
* See https : //github.com/desktop/desktop/issues/2429 for some history on this.
* /
interface IPackageAdditionalOptions {
readonly protocols : ReadonlyArray < {
readonly name : string
2017-08-09 20:47:13 +00:00
readonly schemes : ReadonlyArray < string >
2017-08-09 20:44:41 +00:00
} >
2020-11-28 14:33:13 +00:00
readonly osxSign : OsxSignOptions & {
2019-11-04 16:08:54 +00:00
readonly hardenedRuntime? : boolean
}
2017-08-09 20:44:41 +00:00
}
2018-07-16 19:52:33 +00:00
function packageApp() {
2017-07-28 12:25:05 +00:00
// not sure if this is needed anywhere, so I'm just going to inline it here
// for now and see what the future brings...
2017-07-28 20:30:58 +00:00
const toPackagePlatform = ( platform : NodeJS.Platform ) = > {
2017-08-06 20:35:59 +00:00
if ( platform === 'win32' || platform === 'darwin' || platform === 'linux' ) {
return platform
2017-07-28 12:25:05 +00:00
}
throw new Error (
2020-06-24 18:28:06 +00:00
` Unable to convert to platform for electron-packager: ' ${ process . platform } `
2017-07-28 12:25:05 +00:00
)
}
2020-11-28 14:33:13 +00:00
const toPackageArch = ( targetArch : string | undefined ) : OfficialArch = > {
2018-06-26 12:26:55 +00:00
if ( targetArch === undefined ) {
return 'x64'
2018-05-14 04:13:57 +00:00
}
2018-06-26 12:26:55 +00:00
2018-06-22 13:44:22 +00:00
if ( targetArch === 'arm64' || targetArch === 'x64' ) {
return targetArch
}
2018-05-14 04:13:57 +00:00
throw new Error (
2018-06-26 12:26:55 +00:00
` Building Desktop for architecture ' ${ targetArch } ' is not supported `
2018-05-14 04:13:57 +00:00
)
}
2019-11-03 21:59:36 +00:00
// get notarization deets, unless we're not going to publish this
const notarizationCredentials = isPublishableBuild
? getNotarizationCredentials ( )
: undefined
2019-10-30 17:46:38 +00:00
if (
isPublishableBuild &&
2020-07-09 12:17:24 +00:00
( isCircleCI ( ) || isGitHubActions ( ) ) &&
process . platform === 'darwin' &&
2019-10-30 17:46:38 +00:00
notarizationCredentials === undefined
) {
// we can't publish a mac build without these
throw new Error (
'Unable to retreive appleId and/or appleIdPassword to notarize macOS build'
)
}
2020-07-08 08:41:20 +00:00
const options : Options & IPackageAdditionalOptions = {
2017-09-24 00:01:43 +00:00
name : getExecutableName ( ) ,
2017-07-28 12:25:05 +00:00
platform : toPackagePlatform ( process . platform ) ,
2018-06-22 13:44:22 +00:00
arch : toPackageArch ( process . env . TARGET_ARCH ) ,
2017-04-05 19:34:19 +00:00
asar : false , // TODO: Probably wanna enable this down the road.
2017-09-24 00:01:43 +00:00
out : getDistRoot ( ) ,
2019-11-20 17:44:01 +00:00
icon : path.join ( projectRoot , 'app' , 'static' , 'logos' , getIconFileName ( ) ) ,
2017-04-05 19:34:19 +00:00
dir : outRoot ,
overwrite : true ,
tmpdir : false ,
derefSymlinks : false ,
prune : false , // We'll prune them ourselves below.
ignore : [
2017-07-28 12:25:05 +00:00
new RegExp ( '/node_modules/electron($|/)' ) ,
new RegExp ( '/node_modules/electron-packager($|/)' ) ,
new RegExp ( '/\\.git($|/)' ) ,
new RegExp ( '/node_modules/\\.bin($|/)' ) ,
2017-04-05 19:34:19 +00:00
] ,
2017-07-13 14:21:09 +00:00
appCopyright : 'Copyright © 2017 GitHub, Inc.' ,
2017-04-05 19:34:19 +00:00
// macOS
2017-09-24 00:01:43 +00:00
appBundleId : getBundleID ( ) ,
2017-07-13 14:21:09 +00:00
appCategoryType : 'public.app-category.developer-tools' ,
2019-03-18 21:12:44 +00:00
darwinDarkModeSupport : true ,
2019-11-01 23:41:18 +00:00
osxSign : {
2019-11-03 22:01:59 +00:00
hardenedRuntime : true ,
2019-11-03 22:01:44 +00:00
entitlements : entitlementsPath ,
'entitlements-inherit' : entitlementsPath ,
2019-11-03 22:03:25 +00:00
type : isPublishableBuild ? 'distribution' : 'development' ,
2019-11-01 23:41:18 +00:00
} ,
2019-10-30 17:46:38 +00:00
osxNotarize : notarizationCredentials ,
2017-08-09 20:44:41 +00:00
protocols : [
{
2017-09-24 00:01:43 +00:00
name : getBundleID ( ) ,
2017-08-09 20:44:41 +00:00
schemes : [
2019-11-05 18:01:34 +00:00
! isDevelopmentBuild
2017-08-09 20:44:41 +00:00
? 'x-github-desktop-auth'
: 'x-github-desktop-dev-auth' ,
'x-github-client' ,
'github-mac' ,
] ,
} ,
2017-07-31 13:31:22 +00:00
] ,
2019-11-03 23:09:59 +00:00
extendInfo : extendInfoPath ,
2017-07-31 13:31:22 +00:00
2017-04-05 19:34:19 +00:00
// Windows
win32metadata : {
2017-09-24 00:01:43 +00:00
CompanyName : getCompanyName ( ) ,
2017-07-13 14:21:09 +00:00
FileDescription : '' ,
OriginalFilename : '' ,
2017-09-24 00:01:43 +00:00
ProductName : getProductName ( ) ,
InternalName : getProductName ( ) ,
2017-07-13 14:21:09 +00:00
} ,
2017-04-05 19:34:19 +00:00
}
2018-07-16 19:52:33 +00:00
return packager ( options )
2017-04-05 19:34:19 +00:00
}
2017-08-07 05:01:19 +00:00
function removeAndCopy ( source : string , destination : string ) {
2017-08-06 20:43:02 +00:00
fs . removeSync ( destination )
fs . copySync ( source , destination )
}
2016-09-02 20:45:19 +00:00
2017-08-06 20:43:02 +00:00
function copyEmoji() {
const emojiImages = path . join ( projectRoot , 'gemoji' , 'images' , 'emoji' )
const emojiImagesDestination = path . join ( outRoot , 'emoji' )
removeAndCopy ( emojiImages , emojiImagesDestination )
2016-09-02 20:45:19 +00:00
2017-08-06 20:43:02 +00:00
const emojiJSON = path . join ( projectRoot , 'gemoji' , 'db' , 'emoji.json' )
const emojiJSONDestination = path . join ( outRoot , 'emoji.json' )
removeAndCopy ( emojiJSON , emojiJSONDestination )
2016-09-02 20:45:19 +00:00
}
2017-07-13 14:21:09 +00:00
function copyStaticResources() {
2016-11-02 15:01:19 +00:00
const dirName = process . platform
const platformSpecific = path . join ( projectRoot , 'app' , 'static' , dirName )
const common = path . join ( projectRoot , 'app' , 'static' , 'common' )
const destination = path . join ( outRoot , 'static' )
2016-09-15 19:36:21 +00:00
fs . removeSync ( destination )
2017-07-24 20:44:53 +00:00
if ( fs . existsSync ( platformSpecific ) ) {
fs . copySync ( platformSpecific , destination )
}
2018-04-11 03:41:54 +00:00
fs . copySync ( common , destination , { overwrite : false } )
2016-09-15 19:36:21 +00:00
}
2018-03-29 04:31:45 +00:00
function moveAnalysisFiles() {
const rendererReport = 'renderer.report.html'
const analysisSource = path . join ( outRoot , rendererReport )
if ( fs . existsSync ( analysisSource ) ) {
const distRoot = getDistRoot ( )
const destination = path . join ( distRoot , rendererReport )
fs . mkdirpSync ( distRoot )
// there's no moveSync API here, so let's do it the old fashioned way
//
// unlinkSync below ensures that the analysis file isn't bundled into
// the app by accident
2018-04-11 03:41:54 +00:00
fs . copySync ( analysisSource , destination , { overwrite : true } )
2018-03-29 04:31:45 +00:00
fs . unlinkSync ( analysisSource )
}
}
2017-07-13 14:21:09 +00:00
function copyDependencies() {
2017-07-28 12:25:05 +00:00
const originalPackage : Package = require ( path . join (
projectRoot ,
'app' ,
'package.json'
) )
2017-04-17 21:50:36 +00:00
const oldDependencies = originalPackage . dependencies
2017-07-28 12:25:05 +00:00
const newDependencies : PackageLookup = { }
2017-04-17 21:50:36 +00:00
2017-07-28 12:25:05 +00:00
for ( const name of Object . keys ( oldDependencies ) ) {
const spec = oldDependencies [ name ]
2017-04-17 21:50:36 +00:00
if ( externals . indexOf ( name ) !== - 1 ) {
newDependencies [ name ] = spec
}
}
2017-04-27 20:17:02 +00:00
const oldDevDependencies = originalPackage . devDependencies
2017-07-28 12:25:05 +00:00
const newDevDependencies : PackageLookup = { }
2017-04-27 20:17:02 +00:00
2019-11-05 18:01:34 +00:00
if ( isDevelopmentBuild ) {
2017-07-28 12:25:05 +00:00
for ( const name of Object . keys ( oldDevDependencies ) ) {
const spec = oldDevDependencies [ name ]
2017-04-27 20:17:02 +00:00
if ( externals . indexOf ( name ) !== - 1 ) {
newDevDependencies [ name ] = spec
}
}
}
2016-10-10 21:08:46 +00:00
// The product name changes depending on whether it's a prod build or dev
// build, so that we can have them running side by side.
2017-04-17 21:50:36 +00:00
const updatedPackage = Object . assign ( { } , originalPackage , {
2017-09-24 00:01:43 +00:00
productName : getProductName ( ) ,
2017-04-17 21:50:36 +00:00
dependencies : newDependencies ,
2017-04-27 20:17:02 +00:00
devDependencies : newDevDependencies ,
2017-04-17 21:50:36 +00:00
} )
2019-11-05 18:01:34 +00:00
if ( ! isDevelopmentBuild ) {
2017-04-17 21:50:36 +00:00
delete updatedPackage . devDependencies
}
2016-10-10 21:08:46 +00:00
2017-07-13 14:21:09 +00:00
fs . writeFileSync (
path . join ( outRoot , 'package.json' ) ,
JSON . stringify ( updatedPackage )
)
2016-05-24 18:48:10 +00:00
2017-04-28 06:34:18 +00:00
fs . removeSync ( path . resolve ( outRoot , 'node_modules' ) )
2017-07-13 14:21:09 +00:00
if (
Object . keys ( newDependencies ) . length ||
Object . keys ( newDevDependencies ) . length
) {
2017-11-07 01:12:28 +00:00
console . log ( ' Installing dependencies via yarn…' )
cp . execSync ( 'yarn install' , { cwd : outRoot , env : process.env } )
2017-04-27 20:17:02 +00:00
}
2016-05-24 18:48:10 +00:00
2021-01-28 15:41:17 +00:00
console . log ( ' Copying desktop-trampoline…' )
const desktopTrampolineDir = path . resolve ( outRoot , 'desktop-trampoline' )
const desktopTrampolineFile =
process . platform === 'win32'
? 'desktop-trampoline.exe'
: 'desktop-trampoline'
fs . removeSync ( desktopTrampolineDir )
fs . mkdirSync ( desktopTrampolineDir )
fs . copySync (
path . resolve (
projectRoot ,
'app/node_modules/desktop-trampoline/build/Release' ,
desktopTrampolineFile
) ,
path . resolve ( desktopTrampolineDir , desktopTrampolineFile )
)
2017-04-27 20:16:12 +00:00
console . log ( ' Copying git environment…' )
const gitDir = path . resolve ( outRoot , 'git' )
2017-04-28 06:34:18 +00:00
fs . removeSync ( gitDir )
2017-04-27 20:16:12 +00:00
fs . mkdirpSync ( gitDir )
fs . copySync ( path . resolve ( projectRoot , 'app/node_modules/dugite/git' ) , gitDir )
2017-08-09 02:34:49 +00:00
2017-09-23 09:54:08 +00:00
if ( process . platform === 'win32' ) {
console . log ( ' Cleaning unneeded Git components…' )
const files = [
'Bitbucket.Authentication.dll' ,
'GitHub.Authentication.exe' ,
'Microsoft.Alm.Authentication.dll' ,
'Microsoft.Alm.Git.dll' ,
'Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll' ,
'Microsoft.IdentityModel.Clients.ActiveDirectory.dll' ,
'Microsoft.Vsts.Authentication.dll' ,
'git-askpass.exe' ,
'git-credential-manager.exe' ,
]
const gitCoreDir = path . join ( gitDir , 'mingw64' , 'libexec' , 'git-core' )
for ( const file of files ) {
const filePath = path . join ( gitCoreDir , file )
try {
fs . unlinkSync ( filePath )
} catch ( err ) {
// probably already cleaned up
}
}
}
2017-08-09 02:34:49 +00:00
if ( process . platform === 'darwin' ) {
2017-09-23 09:52:37 +00:00
console . log ( ' Copying app-path binary…' )
2017-08-09 02:34:49 +00:00
const appPathMain = path . resolve ( outRoot , 'main' )
fs . removeSync ( appPathMain )
fs . copySync (
path . resolve ( projectRoot , 'app/node_modules/app-path/main' ) ,
appPathMain
)
}
2016-05-24 18:48:10 +00:00
}
2017-04-05 19:33:46 +00:00
2018-04-03 04:47:22 +00:00
function generateLicenseMetadata ( outRoot : string ) {
2018-03-29 05:43:30 +00:00
const chooseALicense = path . join ( outRoot , 'static' , 'choosealicense.com' )
const licensesDir = path . join ( chooseALicense , '_licenses' )
const files = fs . readdirSync ( licensesDir )
const licenses = new Array < ILicense > ( )
for ( const file of files ) {
const fullPath = path . join ( licensesDir , file )
const contents = fs . readFileSync ( fullPath , 'utf8' )
const result = frontMatter < IChooseALicense > ( contents )
2019-03-23 19:15:07 +00:00
const licenseText = result . body . trim ( )
// ensure that any license file created in the app does not trigger the
// "no newline at end of file" warning when viewing diffs
const licenseTextWithNewLine = ` ${ licenseText } \ n `
2018-03-29 05:43:30 +00:00
const license : ILicense = {
name : result.attributes.nickname || result . attributes . title ,
featured : result.attributes.featured || false ,
hidden :
result . attributes . hidden === undefined || result . attributes . hidden ,
2019-03-23 19:15:07 +00:00
body : licenseTextWithNewLine ,
2018-03-29 05:43:30 +00:00
}
if ( ! license . hidden ) {
licenses . push ( license )
}
}
2018-04-03 04:47:22 +00:00
const licensePayload = path . join ( outRoot , 'static' , 'available-licenses.json' )
2018-03-29 05:43:30 +00:00
const text = JSON . stringify ( licenses )
2018-04-03 04:47:22 +00:00
fs . writeFileSync ( licensePayload , text , 'utf8' )
// embed the license alongside the generated license payload
const chooseALicenseLicense = path . join ( chooseALicense , 'LICENSE.md' )
const licenseDestination = path . join (
outRoot ,
'static' ,
'LICENSE.choosealicense.md'
)
const licenseText = fs . readFileSync ( chooseALicenseLicense , 'utf8' )
const licenseWithHeader = ` GitHub Desktop uses licensing information provided by choosealicense.com.
The bundle in available - licenses . json has been generated from a source list provided at https : //github.com/github/choosealicense.com, which is made available under the below license:
-- -- -- -- -- --
$ { licenseText } `
2018-03-29 05:43:30 +00:00
2018-04-03 04:47:22 +00:00
fs . writeFileSync ( licenseDestination , licenseWithHeader , 'utf8' )
2018-03-29 05:43:30 +00:00
2018-04-03 04:47:22 +00:00
// sweep up the choosealicense directory as the important bits have been bundled in the app
2018-03-29 05:43:30 +00:00
fs . removeSync ( chooseALicense )
}
2019-10-30 17:46:38 +00:00
2020-11-28 14:33:13 +00:00
function getNotarizationCredentials ( ) : OsxNotarizeOptions | undefined {
2019-10-30 17:46:38 +00:00
const appleId = process . env . APPLE_ID
const appleIdPassword = process . env . APPLE_ID_PASSWORD
if ( appleId === undefined || appleIdPassword === undefined ) {
return undefined
}
return {
appleId ,
appleIdPassword ,
}
}