Matt Hough
Matt Hough

Reputation: 1101

How to package an electron app and flask server into one executable

So far (on my mac) I have managed to package my flask app into a single .app file using pyInstaller and can successfully package electron into one .app file. Now I would like to be able to package the flask executable and electron app together into one executable.

I have tried what some other stack overflow posts suggested and used the child_process module to spawn the flask .app, however that gave me the below error:

Uncaught Exception:
Error: spawn ../server/dist/server.app ENOENT
    at _errnoException (util.js:1024:11)
    at Process.ChildProcess._handle.onexit (internal/child_process.js:190:19)
    at onErrorNT (internal/child_process.js:372:16)
    at _combinedTickCallback (internal/process/next_tick.js:138:11)
    at process._tickCallback (internal/process/next_tick.js:180:9)

Here is my electron entry point code that caused this error:

const electron = require('electron');
const app = electron.app;
const BrowserWindow = electron.BrowserWindow;

const isDev = require('electron-is-dev');
const path = require('path');
const childSpawn = require('child_process').spawn;

let mainWindow;

const createWindow = () => {
  childSpawn('../server/dist/server.app');

  mainWindow = new BrowserWindow({ width: 900, height: 680 });
  mainWindow.loadURL(isDev ? 'http://localhost:3000' : `file://${path.join(__dirname, '../build/index.html')}`);

  app.setAboutPanelOptions({
    applicationName: 'app_name',
    applicationVersion: '0.0.1',
  })

  mainWindow.on('closed', () => mainWindow = null);
}

app.on('ready', createWindow);

app.on('window-all-closed', () => {
  app.quit(); 
});

app.on('activate', () => {
  if (mainWindow === null) {
    createWindow();
  }
});

However, if that were to work, I don't see how I could bundle the flask server together with the electron app into one executable?

I'd appreciate some help from someone who has successfully done this.

Upvotes: 4

Views: 4574

Answers (1)

Uday Adiga
Uday Adiga

Reputation: 66

The packaged flask .app is an executable, it cannot be spawned as a child process. You will have to execute the file using execFile. I referred this Following is the snippet I referred from the site.


packaging

Some people are asking for the packaging. This is easy: apply the knowledge of how to package Python applications and Electron applications.

Python part

Useing PyInstaller.

Run the following in the terminal:

pyinstaller pycalc/api.py --distpath pycalcdist

rm -rf build/
rm -rf api.spec

If everything goes well, the pycalcdist/api/ folder should show up, as well as the executable inside that folder. This is the complete independent Python executable that could be moved to somewhere else.

Attention: the independent Python executable has to be generated! Because the target machine we want to distribute to may not have correct Python shell and/or required Python libraries. It’s almost impossible to just copy the Python source codes.

Node.js / Electron part

This is tricky because of the Python executable.

In the above example code, I write

  // part of main.js
  let script = path.join(__dirname, 'pycalc', 'api.py')
  pyProc = require('child_process').spawn('python', [script, port])

However, once we package the Python code, we should no longer spawn Python script. Instead, we should execFile the generated excutable.

Electron doesn’t provide functions to check whether the app is under distributed or not (at least I don’t find it). So I use a workaround here: check whether the Python executable has been generated or not.

In main.js, add the following functions:

// main.js

const PY_DIST_FOLDER = 'pycalcdist'
const PY_FOLDER = 'pycalc'
const PY_MODULE = 'api' // without .py suffix

const guessPackaged = () => {
  const fullPath = path.join(__dirname, PY_DIST_FOLDER)
  return require('fs').existsSync(fullPath)
}

const getScriptPath = () => {
  if (!guessPackaged()) {
    return path.join(__dirname, PY_FOLDER, PY_MODULE + '.py')
  }
  if (process.platform === 'win32') {
    return path.join(__dirname, PY_DIST_FOLDER, PY_MODULE, PY_MODULE + '.exe')
  }
  return path.join(__dirname, PY_DIST_FOLDER, PY_MODULE, PY_MODULE)
}

And change the function createPyProc to this:

// main.js
// the improved version
const createPyProc = () => {
  let script = getScriptPath()
  let port = '' + selectPort()

  if (guessPackaged()) {
    pyProc = require('child_process').execFile(script, [port])
  } else {
    pyProc = require('child_process').spawn('python', [script, port])
  }

  if (pyProc != null) {
    //console.log(pyProc)
    console.log('child process success on port ' + port)
  }
}

The key point is, check whether the *dist folder has been generated or not. If generated, it means we are in “production” mode, execFile the executable directly; otherwise, spawn the script using a Python shell.

Upvotes: 5

Related Questions