Skip to content

Commit d945165

Browse files
committed
Merge remote-tracking branch 'remotes/scratch-desktop/develop' into develop
2 parents 2c38de7 + 0404f9a commit d945165

File tree

10 files changed

+184
-31
lines changed

10 files changed

+184
-31
lines changed

README.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,20 @@ To generate a signed NSIS installer:
7272
4. Build the NSIS installer only: building the APPX installer will fail if these environment variables are set.
7373
- `npm run dist -- -w nsis`
7474

75+
#### Workaround for code signing issue in macOS
76+
77+
Sometimes the macOS build process will result in a build which crashes on startup. If this happens, check in `Console`
78+
for an entry similar to this:
79+
80+
```text
81+
failed to parse entitlements for Smalruby3 Desktop[12345]: OSUnserializeXML: syntax error near line 1
82+
```
83+
84+
This appears to be an issue with `codesign` itself. Rebooting your computer and trying to build again might help. Yes,
85+
really.
86+
87+
See this issue for more detail: <https://github.com/electron/electron-osx-sign/issues/218>
88+
7589
### Make a semi-packaged build
7690

7791
This will simulate a packaged build without actually packaging it: instead the files will be copied to a subdirectory

buildResources/entitlements.plist renamed to buildResources/entitlements.mac.plist

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
33
<plist version="1.0">
44
<dict>
5-
<key>com.apple.security.app-sandbox</key>
5+
<key>com.apple.security.cs.allow-jit</key>
66
<true/>
77
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
88
<true/>

buildResources/entitlements.inherit.plist renamed to buildResources/entitlements.mas.inherit.plist

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,6 @@
44
<dict>
55
<key>com.apple.security.app-sandbox</key>
66
<true/>
7-
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
8-
<true/>
97
<key>com.apple.security.inherit</key>
108
<true/>
119
</dict>

buildResources/entitlements.mas.plist

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3+
<plist version="1.0">
4+
<dict>
5+
<key>com.apple.security.app-sandbox</key>
6+
<true/>
7+
<key>com.apple.security.cs.allow-jit</key>
8+
<true/>
9+
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
10+
<true/>
11+
<key>com.apple.security.device.audio-input</key>
12+
<true/>
13+
<key>com.apple.security.device.camera</key>
14+
<true/>
15+
<key>com.apple.security.device.usb</key>
16+
<true/>
17+
<key>com.apple.security.files.user-selected.read-only</key>
18+
<true/>
19+
<key>com.apple.security.files.user-selected.read-write</key>
20+
<true/>
21+
<key>com.apple.security.network.client</key>
22+
<true/>
23+
<key>com.apple.security.network.server</key>
24+
<true/>
25+
</dict>
26+
</plist>

electron-builder.yaml

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,17 +6,24 @@ productName: "Smalruby3 Desktop"
66
afterSign: "scripts/afterSign.js"
77
mac:
88
category: public.app-category.education
9+
entitlements: buildResources/entitlements.mac.plist
10+
extendInfo:
11+
NSCameraUsageDescription: >-
12+
This app requires camera access when taking a photo in the paint editor or using the video sensing blocks.
13+
NSMicrophoneUsageDescription: >-
14+
This app requires microphone access when recording sounds or detecting loudness.
15+
gatekeeperAssess: true
916
hardenedRuntime: true
1017
icon: buildResources/Smalruby3Desktop.icns
1118
provisioningProfile: embedded.provisionprofile
1219
target:
1320
- dmg
1421
- mas
15-
mas:
1622
type: distribution
23+
mas:
1724
category: public.app-category.education
18-
entitlements: buildResources/entitlements.plist
19-
entitlementsInherit: buildResources/entitlements.inherit.plist
25+
entitlements: buildResources/entitlements.mas.plist
26+
entitlementsInherit: buildResources/entitlements.mas.inherit.plist
2027
icon: buildResources/Smalruby3Desktop.icns
2128
win:
2229
icon: buildResources/Smalruby3Desktop.ico

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@
5858
"react-redux": "5.0.7",
5959
"redux": "3.7.2",
6060
"rimraf": "^2.7.1",
61-
"smalruby3-gui": "github:smalruby/smalruby3-gui#smalruby3-desktop",
61+
"smalruby3-gui": "github:smalruby/smalruby3-gui#smalruby3-desktop-v1.0.0",
6262
"source-map-loader": "^0.2.4",
6363
"uglifyjs-webpack-plugin": "^2.1.3",
6464
"uuid": "^3.4.0",

scripts/electron-builder-wrapper.js

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -51,9 +51,10 @@ const runBuilder = function (targetGroup) {
5151
console.error(`NSIS build requires CSC_LINK or WIN_CSC_LINK`);
5252
}
5353
const platformFlag = getPlatformFlag();
54-
const command = `electron-builder ${platformFlag} ${targetGroup}`;
55-
console.log(`running: ${command}`);
56-
const result = spawnSync(command, {
54+
const customArgs = process.argv.slice(2); // remove `node` and `this-script.js`
55+
const allArgs = [platformFlag, targetGroup, ...customArgs];
56+
console.log(`running electron-builder with arguments: ${allArgs}`);
57+
const result = spawnSync('electron-builder', allArgs, {
5758
env: childEnvironment,
5859
shell: true,
5960
stdio: 'inherit'

src/main/index.js

Lines changed: 125 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
import {BrowserWindow, Menu, app, dialog, ipcMain} from 'electron';
1+
import {BrowserWindow, Menu, app, dialog, ipcMain, systemPreferences} from 'electron';
22
import fs from 'fs';
33
import path from 'path';
4-
import {format as formatUrl} from 'url';
4+
import {URL} from 'url';
55

66
import {getFilterForExtension} from './FileFilters';
77
import MacOSMenu from './MacOSMenu';
@@ -16,6 +16,126 @@ const isDevelopment = process.env.NODE_ENV !== 'production';
1616
// global window references prevent them from being garbage-collected
1717
const _windows = {};
1818

19+
const displayPermissionDeniedWarning = (browserWindow, permissionType) => {
20+
let title;
21+
let message;
22+
switch (permissionType) {
23+
case 'camera':
24+
title = 'Camera Permission Denied';
25+
message = 'Permission to use the camera has been denied. ' +
26+
'Scratch will not be able to take a photo or use video sensing blocks.';
27+
break;
28+
case 'microphone':
29+
title = 'Microphone Permission Denied';
30+
message = 'Permission to use the microphone has been denied. ' +
31+
'Scratch will not be able to record sounds or detect loudness.';
32+
break;
33+
default: // shouldn't ever happen...
34+
title = 'Permission Denied';
35+
message = 'A permission has been denied.';
36+
}
37+
38+
let instructions;
39+
switch (process.platform) {
40+
case 'darwin':
41+
instructions = 'To change Scratch permissions, please check "Security & Privacy" in System Preferences.';
42+
break;
43+
default:
44+
instructions = 'To change Scratch permissions, please check your system settings and restart Scratch.';
45+
break;
46+
}
47+
message = `${message}\n\n${instructions}`;
48+
49+
dialog.showMessageBox(browserWindow, {type: 'warning', title, message});
50+
};
51+
52+
/**
53+
* Build an absolute URL from a relative one, optionally adding search query parameters.
54+
* The base of the URL will depend on whether or not the application is running in development mode.
55+
* @param {string} url - the relative URL, like 'index.html'
56+
* @param {*} search - the optional "search" parameters (the part of the URL after '?'), like "route=about"
57+
* @returns {string} - an absolute URL as a string
58+
*/
59+
const makeFullUrl = (url, search = null) => {
60+
const baseUrl = (isDevelopment ?
61+
`http://localhost:${process.env.ELECTRON_WEBPACK_WDS_PORT}/` :
62+
`file://${__dirname}/`
63+
);
64+
const fullUrl = new URL(url, baseUrl);
65+
if (search) {
66+
fullUrl.search = search; // automatically percent-encodes anything that needs it
67+
}
68+
return fullUrl.toString();
69+
};
70+
71+
/**
72+
* Prompt in a platform-specific way for permission to access the microphone or camera, if Electron supports doing so.
73+
* Any application-level checks, such as whether or not a particular frame or document should be allowed to ask,
74+
* should be done before calling this function.
75+
*
76+
* @param {string} mediaType - one of Electron's media types, like 'microphone' or 'camera'
77+
* @returns {boolean} - true if permission granted, false otherwise.
78+
*/
79+
const askForMediaAccess = async mediaType => {
80+
if (systemPreferences.askForMediaAccess) {
81+
// Electron currently only implements this on macOS
82+
return systemPreferences.askForMediaAccess(mediaType);
83+
}
84+
// For other platforms we can't reasonably do anything other than assume we have access.
85+
return true;
86+
};
87+
88+
const handlePermissionRequest = async (webContents, permission, callback, details) => {
89+
if (webContents !== _windows.main.webContents) {
90+
// deny: request came from somewhere other than the main window's web contents
91+
return callback(false);
92+
}
93+
if (!details.isMainFrame) {
94+
// deny: request came from a subframe of the main window, not the main frame
95+
return callback(false);
96+
}
97+
if (permission !== 'media') {
98+
// deny: request is for some other kind of access like notifications or pointerLock
99+
return callback(false);
100+
}
101+
const requiredBase = makeFullUrl('');
102+
if (details.requestingUrl.indexOf(requiredBase) !== 0) {
103+
// deny: request came from a URL outside of our "sandbox"
104+
return callback(false);
105+
}
106+
let askForMicrophone = false;
107+
let askForCamera = false;
108+
for (const mediaType of details.mediaTypes) {
109+
switch (mediaType) {
110+
case 'audio':
111+
askForMicrophone = true;
112+
break;
113+
case 'video':
114+
askForCamera = true;
115+
break;
116+
default:
117+
// deny: unhandled media type
118+
return callback(false);
119+
}
120+
}
121+
const parentWindow = _windows.main; // if we ever allow media in non-main windows we'll also need to change this
122+
if (askForMicrophone) {
123+
const microphoneResult = await askForMediaAccess('microphone');
124+
if (!microphoneResult) {
125+
displayPermissionDeniedWarning(parentWindow, 'microphone');
126+
return callback(false);
127+
}
128+
}
129+
if (askForCamera) {
130+
const cameraResult = await askForMediaAccess('camera');
131+
if (!cameraResult) {
132+
displayPermissionDeniedWarning(parentWindow, 'camera');
133+
return callback(false);
134+
}
135+
}
136+
return callback(true);
137+
};
138+
19139
const createWindow = ({search = null, url = 'index.html', ...browserWindowOptions}) => {
20140
const window = new BrowserWindow({
21141
useContentSize: true,
@@ -28,25 +148,13 @@ const createWindow = ({search = null, url = 'index.html', ...browserWindowOption
28148
});
29149
const webContents = window.webContents;
30150

151+
webContents.session.setPermissionRequestHandler(handlePermissionRequest);
152+
31153
if (isDevelopment) {
32154
webContents.openDevTools({mode: 'detach', activate: true});
33155
}
34156

35-
const fullUrl = formatUrl(isDevelopment ?
36-
{ // Webpack Dev Server
37-
hostname: 'localhost',
38-
pathname: url,
39-
port: process.env.ELECTRON_WEBPACK_WDS_PORT,
40-
protocol: 'http',
41-
search,
42-
slashes: true
43-
} : { // production / bundled
44-
pathname: path.join(__dirname, url),
45-
protocol: 'file',
46-
search,
47-
slashes: true
48-
}
49-
);
157+
const fullUrl = makeFullUrl(url, search);
50158
window.loadURL(fullUrl);
51159

52160
return window;

src/renderer/app.jsx

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
11
import {ipcRenderer, shell} from 'electron';
22
import bindAll from 'lodash.bindall';
3-
import PropTypes from 'prop-types';
43
import React from 'react';
54
import ReactDOM from 'react-dom';
65
import {compose} from 'redux';
7-
import GUI, {AppStateHOC, TitledHOC} from 'smalruby3-gui';
6+
import GUI, {AppStateHOC} from 'smalruby3-gui';
87

98
import ElectronStorageHelper from '../common/ElectronStorageHelper';
109

0 commit comments

Comments
 (0)