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'
2020-11-19 09:40:38 +00:00
import * as os from 'os'
2023-06-14 08:48:35 +00:00
import packager , { OfficialArch , OsxNotarizeOptions } 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 ,
2021-09-21 16:19:54 +00:00
getDistArchitecture ,
2019-11-04 18:41:06 +00:00
} from './dist-info'
2023-06-20 14:18:08 +00:00
import { 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'
2022-03-04 15:12:05 +00:00
import {
existsSync ,
mkdirSync ,
readdirSync ,
readFileSync ,
rmSync ,
unlinkSync ,
writeFileSync ,
} from 'fs'
import { copySync } from 'fs-extra'
2016-05-16 19:00:56 +00:00
2022-04-05 08:29:19 +00:00
const isPublishableBuild = isPublishable ( )
const isDevelopmentBuild = getChannel ( ) === 'development'
2016-05-16 19:00:56 +00:00
const projectRoot = path . join ( __dirname , '..' )
2022-04-05 08:29:19 +00:00
const entitlementsSuffix = isDevelopmentBuild ? '-dev' : ''
const entitlementsPath = ` ${ projectRoot } /script/entitlements ${ entitlementsSuffix } .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
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…' )
2022-03-04 15:01:36 +00:00
rmSync ( getDistRoot ( ) , { recursive : true , force : true } )
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
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 ) {
2020-11-19 09:49:50 +00:00
targetArch = os . arch ( )
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 (
2020-11-19 09:40:38 +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 &&
2023-06-20 14:18:08 +00:00
isGitHubActions ( ) &&
2020-07-09 12:17:24 +00:00
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'
)
}
2023-06-14 08:48:35 +00:00
return packager ( {
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
] ,
2023-01-17 19:08:53 +00:00
appCopyright : 'Copyright © 2023 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 : {
2023-06-14 08:46:05 +00:00
optionsForFile : ( path : string ) = > ( {
hardenedRuntime : true ,
entitlements : entitlementsPath ,
} ) ,
2019-11-03 22:03:25 +00:00
type : isPublishableBuild ? 'distribution' : 'development' ,
2022-04-05 08:29:19 +00:00
// For development, we will use '-' as the identifier so that codesign
// will sign the app to run locally. We need to disable 'identity-validation'
// or otherwise it will replace '-' with one of the regular codesigning
// identities in our system.
identity : isDevelopmentBuild ? '-' : undefined ,
2023-06-14 08:46:05 +00:00
identityValidation : ! isDevelopmentBuild ,
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
} ,
2023-06-14 08:48:35 +00:00
} )
2017-04-05 19:34:19 +00:00
}
2017-08-07 05:01:19 +00:00
function removeAndCopy ( source : string , destination : string ) {
2022-03-04 15:01:36 +00:00
rmSync ( destination , { recursive : true , force : true } )
2022-03-04 15:12:05 +00:00
copySync ( source , destination )
2017-08-06 20:43:02 +00:00
}
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' )
2022-03-04 15:01:36 +00:00
rmSync ( destination , { recursive : true , force : true } )
2022-03-04 15:12:05 +00:00
if ( existsSync ( platformSpecific ) ) {
copySync ( platformSpecific , destination )
2017-07-24 20:44:53 +00:00
}
2022-03-04 15:12:05 +00:00
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 )
2022-03-04 15:12:05 +00:00
if ( existsSync ( analysisSource ) ) {
2018-03-29 04:31:45 +00:00
const distRoot = getDistRoot ( )
const destination = path . join ( distRoot , rendererReport )
2022-03-04 15:12:05 +00:00
mkdirSync ( distRoot , { recursive : true } )
2018-03-29 04:31:45 +00:00
// 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
2022-03-04 15:12:05 +00:00
copySync ( analysisSource , destination , { overwrite : true } )
unlinkSync ( analysisSource )
2018-03-29 04:31:45 +00:00
}
}
2017-07-13 14:21:09 +00:00
function copyDependencies() {
2022-01-26 14:02:11 +00:00
const pkg : Package = require ( path . join ( projectRoot , 'app' , 'package.json' ) )
2017-04-17 21:50:36 +00:00
2022-01-26 14:02:11 +00:00
const filterExternals = ( dependencies : Record < string , string > ) = >
Object . fromEntries (
Object . entries ( dependencies ) . filter ( ( [ k ] ) = > externals . includes ( k ) )
)
2017-04-27 20:17:02 +00:00
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.
2022-01-26 14:02:11 +00:00
pkg . productName = getProductName ( )
pkg . dependencies = filterExternals ( pkg . dependencies )
pkg . devDependencies =
isDevelopmentBuild && pkg . devDependencies
? filterExternals ( pkg . devDependencies )
: { }
2022-03-04 15:12:05 +00:00
writeFileSync ( path . join ( outRoot , 'package.json' ) , JSON . stringify ( pkg ) )
2022-03-04 15:01:36 +00:00
rmSync ( path . resolve ( outRoot , 'node_modules' ) , {
recursive : true ,
force : true ,
} )
2017-04-28 06:34:18 +00:00
2022-01-26 14:02:11 +00:00
console . log ( ' Installing dependencies via yarn…' )
cp . execSync ( 'yarn install' , { cwd : outRoot , env : process.env } )
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'
2022-03-04 15:01:36 +00:00
rmSync ( desktopTrampolineDir , { recursive : true , force : true } )
2022-03-04 15:12:05 +00:00
mkdirSync ( desktopTrampolineDir , { recursive : true } )
copySync (
2021-01-28 15:41:17 +00:00
path . resolve (
projectRoot ,
'app/node_modules/desktop-trampoline/build/Release' ,
desktopTrampolineFile
) ,
path . resolve ( desktopTrampolineDir , desktopTrampolineFile )
)
2021-08-18 10:18:57 +00:00
// Dev builds for macOS require a SSH wrapper to use SSH_ASKPASS
if ( process . platform === 'darwin' && isDevelopmentBuild ) {
2021-08-17 10:56:27 +00:00
console . log ( ' Copying ssh-wrapper' )
const sshWrapperFile = 'ssh-wrapper'
2022-03-04 15:12:05 +00:00
copySync (
2021-08-17 10:56:27 +00:00
path . resolve (
projectRoot ,
'app/node_modules/desktop-trampoline/build/Release' ,
sshWrapperFile
) ,
path . resolve ( desktopTrampolineDir , sshWrapperFile )
)
}
2017-04-27 20:16:12 +00:00
console . log ( ' Copying git environment…' )
const gitDir = path . resolve ( outRoot , 'git' )
2022-03-04 15:01:36 +00:00
rmSync ( gitDir , { recursive : true , force : true } )
2022-03-04 15:12:05 +00:00
mkdirSync ( gitDir , { recursive : true } )
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' ,
2021-09-21 16:19:54 +00:00
'WebView2Loader.dll' ,
2017-09-23 09:54:08 +00:00
]
2021-09-21 16:19:54 +00:00
const mingwFolder = getDistArchitecture ( ) === 'x64' ? 'mingw64' : 'mingw32'
const gitCoreDir = path . join ( gitDir , mingwFolder , 'libexec' , 'git-core' )
2017-09-23 09:54:08 +00:00
for ( const file of files ) {
const filePath = path . join ( gitCoreDir , file )
try {
2022-03-04 15:12:05 +00:00
unlinkSync ( filePath )
2017-09-23 09:54:08 +00:00
} 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' )
2022-03-04 15:01:36 +00:00
rmSync ( appPathMain , { recursive : true , force : true } )
2022-03-04 15:12:05 +00:00
copySync (
2017-08-09 02:34:49 +00:00
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' )
2022-03-04 15:12:05 +00:00
const files = readdirSync ( licensesDir )
2018-03-29 05:43:30 +00:00
const licenses = new Array < ILicense > ( )
for ( const file of files ) {
const fullPath = path . join ( licensesDir , file )
2022-03-04 15:12:05 +00:00
const contents = readFileSync ( fullPath , 'utf8' )
2018-03-29 05:43:30 +00:00
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 )
2022-03-04 15:12:05 +00:00
writeFileSync ( licensePayload , text , 'utf8' )
2018-04-03 04:47:22 +00:00
// embed the license alongside the generated license payload
const chooseALicenseLicense = path . join ( chooseALicense , 'LICENSE.md' )
const licenseDestination = path . join (
outRoot ,
'static' ,
'LICENSE.choosealicense.md'
)
2022-03-04 15:12:05 +00:00
const licenseText = readFileSync ( chooseALicenseLicense , 'utf8' )
2018-04-03 04:47:22 +00:00
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
2022-03-04 15:12:05 +00:00
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
2022-03-04 15:01:36 +00:00
rmSync ( chooseALicense , { recursive : true , force : true } )
2018-03-29 05:43:30 +00:00
}
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 ,
}
}