Skip to content

Commit

Permalink
Package NVDA for the Windows Store (#7851)
Browse files Browse the repository at this point in the history
* Allow Building an appx package by running scons appx
* appveyor should also build the appx package
* Disable features that are incompatible with Windows Store policy
* Restart on Windows store updates
* Change publisher to match Windows store, and set appx version so  minor is build and revision is 0
* Hardcode certain Windows Store publisher details, and don't sign the appx anymore (Windows Store does that).
* Change back the publisher to NV Access in appveyor.yml.  It is now hardcoded separately in the appx manifest for windows store.
* scons appx now produces two appx files:
 * storeSubmition: not signed, and a publisher ID matching NV Access Limited's Store publisher ID. Suitable for submitting to the Windows Store via NV Access Limited's developer account
 * sideLoadable: signed by NV Access Limited, suitable for installing on a Windows 10 system manually as a side-loaded app for testing.
  • Loading branch information
michaelDCurran committed Jan 4, 2018
1 parent 8cfdfe2 commit 2571d54
Show file tree
Hide file tree
Showing 15 changed files with 228 additions and 24 deletions.
2 changes: 1 addition & 1 deletion appveyor.yml
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ install:

build_script:
- ps: |
$sconsOutTargets = "launcher"
$sconsOutTargets = "launcher appx"
$sconsArgs = "version=$env:version"
if ($env:release) {
$sconsOutTargets += " changes userGuide developerGuide"
Expand Down
Binary file added appx/appx_images/nvda_150x150.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added appx/appx_images/nvda_44x44.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
47 changes: 47 additions & 0 deletions appx/manifest.xml.subst
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
<?xml version="1.0" encoding="utf-8"?>
<Package
xmlns="http://schemas.microsoft.com/appx/manifest/foundation/windows10"
xmlns:uap="http://schemas.microsoft.com/appx/manifest/uap/windows10"
xmlns:rescap="http://schemas.microsoft.com/appx/manifest/foundation/windows10/restrictedcapabilities"
>
<Identity
Name="%packageName%"
Version="%packageVersion%"
Publisher="%packagePublisher%"
ProcessorArchitecture="x86"
/>
<Properties>
<DisplayName>%productName%</DisplayName>
<PublisherDisplayName>%publisher%</PublisherDisplayName>
<Description>%description%</Description>
<Logo>appx_images/nvda_44x44.png</Logo>
</Properties>
<Resources>
<Resource Language="en" />
</Resources>
<Dependencies>
<TargetDeviceFamily
Name="Windows.Desktop"
MinVersion="10.0.15063.0"
MaxVersionTested="10.0.16278.100"
/>
</Dependencies>
<Capabilities>
<rescap:Capability Name="runFullTrust"/>
</Capabilities>
<Applications>
<Application
Id="mainExecutable"
Executable="nvda_noUIAccess.exe"
EntryPoint="Windows.FullTrustApplication"
>
<uap:VisualElements
DisplayName="%productName%"
Description="%description%"
Square150x150Logo="appx_images/nvda_150x150.png"
Square44x44Logo="appx_images/nvda_44x44.png"
BackgroundColor="#660099"
/>
</Application>
</Applications>
</Package>
97 changes: 97 additions & 0 deletions appx/sconscript
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import subprocess
import versionInfo

Import([
'env',
'outFilePrefix',
'isStoreSubmission',
])

def getCertPublisher(env):
"""
If no signing certificate is provided, then the given publisher is used as is.
If a signing certificate is given, then the publisher is extracted from the certificate.
"""
certFile=env.get('certFile')
if not certFile:
return env['publisher']
certPassword=env.get('certPassword','')
cmd=['certutil','-dump','-p',certPassword,File('#'+certFile).abspath.replace('/','\\')]
lines=subprocess.check_output(cmd).splitlines()
linePrefix='Subject: '
for line in lines:
if line.startswith(linePrefix):
subject=line[len(linePrefix):].rstrip()
return subject

packageName="NVAccessLimited.NVDANonVisualDesktopAccess"
packageVersion="%s.%s.%s.%s"%(versionInfo.version_year,versionInfo.version_major,env['version_build'],0)
if isStoreSubmission:
packageFileName=outFilePrefix+"_storeSubmission.appx"
# NV Access Limited's Windows Store publisher ID
# It is okay to be here as the only way to submit, validate and sign the package is via the NV Access store account.
packagePublisher="CN=83B1DA31-9B66-442C-88AB-77B4B815E1DE"
packagePublisherDisplayName="NV Access Limited"
productName="NVDA Screen Reader (Windows Store Edition)"
else: # not for submission, just side-loadable
packageFileName=outFilePrefix+"_sideLoadable.appx"
packagePublisher=getCertPublisher(env)
packagePublisherDisplayName=env['publisher']
productName="NVDA Screen Reader (Windows Desktop Bridge Edition)"

signExec=env['signExec'] if env['certFile'] else None

# Files from NVDA's distribution that cannot be included in the appx due to policy or security restrictions
excludedDistFiles=[
'nvda_eoaProxy.exe',
'nvda_service.exe',
'nvda_slave.exe',
'nvda_uiAccess.exe',
'lib/IAccessible2Proxy.dll',
'lib/ISimpleDOM.dll',
'lib/minHook.dll',
'lib/NVDAHelperRemote.dll',
'lib/VBufBackend_adobeAcrobat.dll',
'lib/VBufBackend_adobeFlash.dll',
'lib/VBufBackend_gecko_ia2.dll',
'lib/VBufBackend_lotusNotesRichText.dll',
'lib/VBufBackend_mshtml.dll',
'lib/VBufBackend_webKit.dll',
'lib64/',
'uninstall.exe',
]

# Create an appx manifest with version and publisher etc all filled in
manifest=env.Substfile(
"AppxManifest.xml",
'manifest.xml.subst',
SUBST_DICT={
'%packageName%':packageName,
'%packageVersion%':packageVersion,
'%packagePublisher%':packagePublisher,
'%publisher%':packagePublisherDisplayName,
'%productName%':productName,
'%description%':versionInfo.description,
},
)
# Make a copy of the dist dir produced by py2exe
# And also place some extra appx specific images in there
appxContent=env.Command(
target='content',
source=[Dir("#dist"),Dir('#appx/appx_images'),manifest],
action=[
Delete("$TARGET"),
Copy("$TARGET","${SOURCES[0]}"),
Copy("${TARGET}\\appx_images","${SOURCES[1]}"),
Copy("${TARGET}\\AppxManifest.xml","${SOURCES[2]}"),
]+[Delete("${TARGET}/%s"%excludeFile) for excludeFile in excludedDistFiles],
)
# Ensure that it is always copied as we can't tell if dist changed
env.AlwaysBuild(appxContent)
# Package the appx
appx=env.Command(packageFileName,appxContent,"makeappx pack /p $TARGET /d $SOURCE")
if signExec and not isStoreSubmission:
env.AddPostAction(appx,signExec)

Return(['appx'])

11 changes: 10 additions & 1 deletion sconstruct
Original file line number Diff line number Diff line change
Expand Up @@ -118,11 +118,14 @@ sourceLibDir64=sourceDir.Dir('lib64')
Export('sourceLibDir64')
buildDir = Dir("build")
outFilePrefix = "nvda{type}_{version}".format(type="" if release else "_snapshot", version=version)
Export('outFilePrefix')
outputDir=Dir(env['outputDir'])
Export('outputDir')
devDocsOutputDir=outputDir.Dir('devDocs')

# An action to sign an executable with certFile.
signExecCmd = ["signtool", "sign", "/f", certFile]
# we encrypt with SHA256 as this is the minimum required by the Windows Store for appx packages
signExecCmd = ["signtool", "sign", "/fd", "SHA256", "/f", certFile]
if certPassword:
signExecCmd.extend(("/p", certPassword))
if certTimestampServer:
Expand Down Expand Up @@ -389,6 +392,12 @@ symbolsList.extend(env.Glob(os.path.join(sourceLibDir64.path,'*.pdb')))
symbolsArchive = env.ZipArchive(outputDir.File("%s_debugSymbols.zip" % outFilePrefix), symbolsList)
env.Alias("symbolsArchive", symbolsArchive)

appx_storeSubmission=env.SConscript("appx/sconscript",exports={'env':env,'isStoreSubmission':True},variant_dir='build\\appx_storeSubmission')
installed_appx_storeSubmission=env.Install('output',appx_storeSubmission)
appx_sideLoadable=env.SConscript("appx/sconscript",exports={'env':env,'isStoreSubmission':False},variant_dir='build\\appx_sideLoadable')
installed_appx_sideLoadable=env.Install('output',appx_sideLoadable)
env.Alias('appx',[installed_appx_storeSubmission,installed_appx_sideLoadable])

env.Default(dist)

env.SConscript("tests/sconscript", exports=["env", "sourceDir", "pot"])
21 changes: 13 additions & 8 deletions source/NVDAHelper.py
Original file line number Diff line number Diff line change
Expand Up @@ -452,10 +452,14 @@ def initialize():
generateBeep=localLib.generateBeep
generateBeep.argtypes=[c_char_p,c_float,c_int,c_int,c_int]
generateBeep.restype=c_int
# The rest of this function (to do with injection only applies if NVDA is not running as a Windows store application)
# Handle VBuf_getTextInRange's BSTR out parameter so that the BSTR will be freed automatically.
VBuf_getTextInRange = CFUNCTYPE(c_int, c_int, c_int, c_int, POINTER(BSTR), c_int)(
("VBuf_getTextInRange", localLib),
((1,), (1,), (1,), (2,), (1,)))
if config.isAppX:
log.info("Remote injection disabled due to running as a Windows Store Application")
return
#Load nvdaHelperRemote.dll but with an altered search path so it can pick up other dlls in lib
h=windll.kernel32.LoadLibraryExW(os.path.abspath(os.path.join(versionedLibPath,u"nvdaHelperRemote.dll")),0,0x8)
if not h:
Expand All @@ -473,14 +477,15 @@ def initialize():

def terminate():
global _remoteLib, _remoteLoader64, localLib, generateBeep, VBuf_getTextInRange
if not _remoteLib.uninstallIA2Support():
log.debugWarning("Error uninstalling IA2 support")
if _remoteLib.injection_terminate() == 0:
raise RuntimeError("Error terminating NVDAHelperRemote")
_remoteLib=None
if _remoteLoader64:
_remoteLoader64.terminate()
_remoteLoader64=None
if not config.isAppX:
if not _remoteLib.uninstallIA2Support():
log.debugWarning("Error uninstalling IA2 support")
if _remoteLib.injection_terminate() == 0:
raise RuntimeError("Error terminating NVDAHelperRemote")
_remoteLib=None
if _remoteLoader64:
_remoteLoader64.terminate()
_remoteLoader64=None
generateBeep=None
VBuf_getTextInRange=None
localLib.nvdaHelperLocal_terminate()
Expand Down
3 changes: 3 additions & 0 deletions source/addonHandler.py
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,9 @@ def disableAddonsIfAny():

def initialize():
""" Initializes the add-ons subsystem. """
if config.isAppX:
log.info("Add-ons not supported when running as a Windows Store application")
return
loadState()
removeFailedDeletions()
completePendingAddonRemoves()
Expand Down
19 changes: 16 additions & 3 deletions source/config/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@
import profileUpgrader
from .configSpec import confspec

#: True if NVDA is running as a Windows Store Desktop Bridge application
isAppX=False

#: The active configuration, C{None} if it has not yet been loaded.
#: @type: ConfigObj
conf = None
Expand Down Expand Up @@ -98,7 +101,14 @@ def getUserDefaultConfigPath(useInstalledPathIfExists=False):
Most callers will want the C{globalVars.appArgs.configPath variable} instead.
"""
installedUserConfigPath=getInstalledUserConfigPath()
if installedUserConfigPath and (isInstalledCopy() or (useInstalledPathIfExists and os.path.isdir(installedUserConfigPath))):
if installedUserConfigPath and (isInstalledCopy() or isAppX or (useInstalledPathIfExists and os.path.isdir(installedUserConfigPath))):
if isAppX:
# NVDA is running as a Windows Store application.
# Although Windows will redirect %APPDATA% to a user directory specific to the Windows Store application,
# It also makes existing %APPDATA% files available here.
# We cannot share NVDA user config directories with other copies of NVDA as their config may be using add-ons
# Therefore add a suffix to the directory to make it specific to Windows Store application versions.
installedUserConfigPath+='_appx'
return installedUserConfigPath
return u'.\\userConfig\\'

Expand All @@ -120,7 +130,10 @@ def initConfigPath(configPath=None):
configPath=globalVars.appArgs.configPath
if not os.path.isdir(configPath):
os.makedirs(configPath)
for subdir in ("addons", "appModules","brailleDisplayDrivers","speechDicts","synthDrivers","globalPlugins","profiles"):
subdirs=["speechDicts","profiles"]
if not isAppX:
subdirs.extend(["addons", "appModules","brailleDisplayDrivers","synthDrivers","globalPlugins"])
for subdir in subdirs:
subdir=os.path.join(configPath,subdir)
if not os.path.isdir(subdir):
os.makedirs(subdir)
Expand Down Expand Up @@ -272,7 +285,7 @@ def addConfigDirsToPythonPackagePath(module, subdir=None):
@param subdir: The subdirectory to be used, C{None} for the name of C{module}.
@type subdir: str
"""
if globalVars.appArgs.disableAddons:
if isAppX or globalVars.appArgs.disableAddons:
return
if not subdir:
subdir = module.__name__
Expand Down
9 changes: 7 additions & 2 deletions source/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -221,8 +221,13 @@ def OnAssert(self,file,line,cond,msg):
message="{file}, line {line}:\nassert {cond}: {msg}".format(file=file,line=line,cond=cond,msg=msg)
log.debugWarning(message,codepath="WX Widgets",stack_info=True)
app = App(redirect=False)
# We do support QueryEndSession events, but we don't want to do anything for them.
app.Bind(wx.EVT_QUERY_END_SESSION, lambda evt: None)
# We support queryEndSession events, but in general don't do anything for them.
# However, when running as a Windows Store application, we do want to request to be restarted for updates
def onQueryEndSession(evt):
if config.isAppX:
# Automatically restart NVDA on Windows Store update
ctypes.windll.kernel32.RegisterApplicationRestart(None,0)
app.Bind(wx.EVT_QUERY_END_SESSION, onQueryEndSession)
def onEndSession(evt):
# NVDA will be terminated as soon as this function returns, so save configuration if appropriate.
config.saveOnExit()
Expand Down
2 changes: 1 addition & 1 deletion source/globalCommands.py
Original file line number Diff line number Diff line change
Expand Up @@ -1655,7 +1655,7 @@ def script_revertConfiguration(self,gesture):
script_revertConfiguration.category=SCRCAT_CONFIG

def script_activatePythonConsole(self,gesture):
if globalVars.appArgs.secure:
if globalVars.appArgs.secure or config.isAppX:
return
import pythonConsole
if not pythonConsole.consoleUI:
Expand Down
13 changes: 7 additions & 6 deletions source/gui/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -405,24 +405,25 @@ def __init__(self, frame):
# Translators: The label for the menu item to toggle Speech Viewer.
item=self.menu_tools_toggleSpeechViewer = menu_tools.AppendCheckItem(wx.ID_ANY, _("Speech viewer"))
self.Bind(wx.EVT_MENU, frame.onToggleSpeechViewerCommand, item)
if not globalVars.appArgs.secure:
if not globalVars.appArgs.secure and not config.isAppX:
# Translators: The label for the menu item to open NVDA Python Console.
item = menu_tools.Append(wx.ID_ANY, _("Python console"))
self.Bind(wx.EVT_MENU, frame.onPythonConsoleCommand, item)
# Translators: The label of a menu item to open the Add-ons Manager.
item = menu_tools.Append(wx.ID_ANY, _("Manage &add-ons..."))
self.Bind(wx.EVT_MENU, frame.onAddonsManagerCommand, item)
if not globalVars.appArgs.secure and getattr(sys,'frozen',None):
if not globalVars.appArgs.secure and not config.isAppX and getattr(sys,'frozen',None):
# Translators: The label for the menu item to create a portable copy of NVDA from an installed or another portable version.
item = menu_tools.Append(wx.ID_ANY, _("Create portable copy..."))
self.Bind(wx.EVT_MENU, frame.onCreatePortableCopyCommand, item)
if not config.isInstalledCopy():
# Translators: The label for the menu item to install NVDA on the computer.
item = menu_tools.Append(wx.ID_ANY, _("&Install NVDA..."))
self.Bind(wx.EVT_MENU, frame.onInstallCommand, item)
# Translators: The label for the menu item to reload plugins.
item = menu_tools.Append(wx.ID_ANY, _("Reload plugins"))
self.Bind(wx.EVT_MENU, frame.onReloadPluginsCommand, item)
if not config.isAppX:
# Translators: The label for the menu item to reload plugins.
item = menu_tools.Append(wx.ID_ANY, _("Reload plugins"))
self.Bind(wx.EVT_MENU, frame.onReloadPluginsCommand, item)
# Translators: The label for the Tools submenu in NVDA menu.
self.menu.AppendMenu(wx.ID_ANY, _("Tools"), menu_tools)

Expand Down Expand Up @@ -625,7 +626,7 @@ def __init__(self, parent):
startAfterLogonText = _("&Automatically start NVDA after I log on to Windows")
self.startAfterLogonCheckBox = sHelper.addItem(wx.CheckBox(self, label=startAfterLogonText))
self.startAfterLogonCheckBox.Value = config.getStartAfterLogon()
if globalVars.appArgs.secure or not config.isInstalledCopy():
if globalVars.appArgs.secure or config.isAppX or not config.isInstalledCopy():
self.startAfterLogonCheckBox.Disable()
# Translators: The label of a checkbox in the Welcome dialog.
showWelcomeDialogAtStartupText = _("&Show this dialog when NVDA starts")
Expand Down
9 changes: 9 additions & 0 deletions source/gui/addonGui.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import os
import wx
import core
import config
import languageHandler
import gui
from logHandler import log
Expand Down Expand Up @@ -308,6 +309,14 @@ def __del__(self):

@classmethod
def handleRemoteAddonInstall(cls, addonPath):
# Add-ons cannot be installed into a Windows store version of NVDA
if config.isAppX:
# Translators: The message displayed when an add-on cannot be installed due to NVDA running as a Windows Store app
gui.messageBox(_("Add-ons cannot be installed in the Windows Store version of NVDA"),
# Translators: The title of a dialog presented when an error occurs.
_("Error"),
wx.OK | wx.ICON_ERROR)
return
closeAfter = AddonsDialog._instance is None
dialog = AddonsDialog(gui.mainFrame)
dialog.installAddon(addonPath, closeAfter=closeAfter)
Expand Down
Loading

0 comments on commit 2571d54

Please sign in to comment.