├── .gitignore ├── LICENSE ├── README.md ├── examples ├── cliapp │ ├── main.py │ ├── requirements.txt │ └── van.py └── flaskapp │ ├── main.py │ ├── static │ ├── jquery-3.4.1.min.js │ ├── metro-all.min.css │ ├── metro.min.js │ ├── scripts.js │ └── styles.css │ ├── templates │ └── index.html │ └── van.py ├── pyvan.png ├── setup.py └── src └── pyvan.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 98 | __pypackages__/ 99 | 100 | # Celery stuff 101 | celerybeat-schedule 102 | celerybeat.pid 103 | 104 | # SageMath parsed files 105 | *.sage.py 106 | 107 | # Environments 108 | .env 109 | .venv 110 | env/ 111 | venv/ 112 | ENV/ 113 | env.bak/ 114 | venv.bak/ 115 | 116 | # Spyder project settings 117 | .spyderproject 118 | .spyproject 119 | 120 | # Rope project settings 121 | .ropeproject 122 | 123 | # mkdocs documentation 124 | /site 125 | 126 | # mypy 127 | .mypy_cache/ 128 | .dmypy.json 129 | dmypy.json 130 | 131 | # Pyre type checker 132 | .pyre/ 133 | 134 | # pytype static type analyzer 135 | .pytype/ 136 | 137 | # Cython debug symbols 138 | cython_debug/ 139 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Alin Climente 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |

4 | 5 |

6 |

Make runnable desktop/cmd apps from your python scripts!

7 |

8 | 9 | 10 | [![Downloads](https://pepy.tech/badge/pyvan)](https://pepy.tech/project/pyvan) [![PyPI](https://img.shields.io/pypi/v/pyvan?color=blue)](https://pypi.org/project/pyvan/) 11 | 12 | 13 | ### Install 14 | ```py 15 | pip install pyvan 16 | ``` 17 | Make sure you have Python 3.8 or above. 18 | 19 | ### Usage 20 | 21 | Using the command line: 22 | ```py 23 | pyvan main.py 24 | ``` 25 | For a gui application add `--no-console` flag: 26 | 27 | ```py 28 | pyvan main.py --no-console 29 | ``` 30 | 31 | You can see available flags with: 32 | 33 | ```py 34 | pyvan --help 35 | ``` 36 | 37 | *Or* 38 | 39 | Make a "van.py" file next to the "main.py" file (entry point of your program) 40 | 41 | Paste the code bellow: 42 | 43 | ```python 44 | 45 | import pyvan 46 | 47 | OPTIONS = { 48 | "main_file_name": "main.py", 49 | "show_console": False, 50 | "use_existing_requirements": True, 51 | "extra_pip_install_args": [], 52 | "python_version": None, 53 | "use_pipreqs": False, 54 | "install_only_these_modules": [], 55 | "exclude_modules": [], 56 | "include_modules": [], 57 | "path_to_get_pip_and_python_embedded_zip": "", 58 | "build_dir": "dist", 59 | "pydist_sub_dir": "pydist", 60 | "source_sub_dir": "", 61 | "icon_file": None, 62 | "verbose": True, 63 | } 64 | 65 | pyvan.build(**OPTIONS) 66 | 67 | 68 | ``` 69 | 70 | 71 | ### Configurations 72 | 73 | **Option**|**Default**|**Description** 74 | -----|-----|----- 75 | main\_file\_name|*required*|the entry point of the application 76 | show\_console|True|show console window or not (for a service or GUI app) 77 | use\_existing\_requirements|True|if True pyvan will use an existing requirements.txt file instead of generating one using the: `use\_pipreqs 78 | extra\_pip\_install\_args|[]|pyvan will append the provided arguments to the pip install command during installation of the stand-alone distribution.The arguments should be specified as a list of strings 79 | python\_version|None|pyvan will attempt use the specified Python distribution for creating the stand-alone application, `3.8.x`, `3.9.1`, or `x.x.x` are valid formats 80 | use\_pipreqs|True|pipreqs tries to minimize the size of your app by looking at your imports (best way is to use a virtualenv to ensure a smaller size 81 | install\_only\_these\_modules|[]|pyvan will install only the modules mentioned here 82 | exclude\_modules|[]|modules to exclude from bundle 83 | include\_modules|[]|modules to include in the bundle 84 | path\_to\_get\_pip\_and\_python\_embedded\_zip|''|by default is the Download path (path to 'get-pip.py' and 'python-x.x.x-embed-amdxx.zip' files) 85 | build\_dir|dist|the directory in which pyvan will create the stand-alone distribution 86 | pydist\_sub\_dir|pydist|a sub directory relative to `build_dir` where the stand-alone python distribution will be installed 87 | source\_sub\_dir|''|a sub directory relative to `build_dir` where the to execute python files will be installed 88 | input\_dir|'.'|the directory to get the main\_file\_name file from 89 | icon\_file|None|path to icon file to use for your application executable, doesn't use one by default 90 | verbose|True|Limit the amount of logs you see in the terminal. Show only warnings or errors. 91 | 92 | 93 | 94 | 95 | **Thanks to [silvandeleemput](https://github.com/silvandeleemput) for extending the available options, adding support for CLI commands, automating the download of get-pip.py, 96 | embedded python zip and making possible the generation of an executable file!** 97 | 98 | I think pyvan is the only python bundler which makes possible shipping a python application along with a modifiable source code. 99 | 100 | 101 | If pyvan didn't manage to install all the modules needed go in dist/Scripts folder and install them manually with `pip install module` 102 | 103 | Since Mac and Linux have already Python installed pyvan focuses only on Windows. 104 | 105 | 106 | ### Why pyvan? 107 | 108 | **pyvan** it's just one file which takes the embedded python version, installs the modules you need and makes a link using a .exe file between python.exe and your main.py script. 109 |
110 | It's easy if something goes wrong for whatever reason you can just go in the dist folder and solve the issue the python way (because there is just python and your scripts :). 111 | 112 | 113 | **Submit any questions/issues you have! Fell free to fork it and improve it!** 114 | -------------------------------------------------------------------------------- /examples/cliapp/main.py: -------------------------------------------------------------------------------- 1 | import click 2 | 3 | 4 | @click.command() 5 | @click.argument( 6 | "numbers", 7 | nargs=-1, 8 | type=float 9 | ) 10 | def cli(numbers): 11 | if len(numbers) == 0: 12 | click.echo("Enter one or more numbers.") 13 | return 14 | click.echo("Computing the sum of the entered numbers:") 15 | click.echo(sum(numbers)) 16 | 17 | 18 | if __name__ == "__main__": 19 | cli() 20 | -------------------------------------------------------------------------------- /examples/cliapp/requirements.txt: -------------------------------------------------------------------------------- 1 | click 2 | -------------------------------------------------------------------------------- /examples/cliapp/van.py: -------------------------------------------------------------------------------- 1 | import pyvan 2 | 3 | 4 | OPTIONS = { 5 | "main_file_name": "main.py", 6 | "show_console": True, 7 | "use_existing_requirements": True, 8 | } 9 | 10 | 11 | pyvan.build(**OPTIONS) 12 | -------------------------------------------------------------------------------- /examples/flaskapp/main.py: -------------------------------------------------------------------------------- 1 | from flask import Flask 2 | from flask import render_template 3 | from flaskwebgui import FlaskUI 4 | 5 | 6 | app = Flask(__name__) 7 | ui = FlaskUI(app, width=500, height=500) 8 | 9 | 10 | @app.route("/") 11 | def hello(): 12 | return render_template('index.html') 13 | 14 | 15 | @app.route("/home", methods=['GET']) 16 | def home(): 17 | return "Home" 18 | 19 | 20 | if __name__ == "__main__": 21 | ui.run() 22 | -------------------------------------------------------------------------------- /examples/flaskapp/static/jquery-3.4.1.min.js: -------------------------------------------------------------------------------- 1 | /*! jQuery v3.4.1 | (c) JS Foundation and other contributors | jquery.org/license */ 2 | !function(e,t){"use strict";"object"==typeof module&&"object"==typeof module.exports?module.exports=e.document?t(e,!0):function(e){if(!e.document)throw new Error("jQuery requires a window with a document");return t(e)}:t(e)}("undefined"!=typeof window?window:this,function(C,e){"use strict";var t=[],E=C.document,r=Object.getPrototypeOf,s=t.slice,g=t.concat,u=t.push,i=t.indexOf,n={},o=n.toString,v=n.hasOwnProperty,a=v.toString,l=a.call(Object),y={},m=function(e){return"function"==typeof e&&"number"!=typeof e.nodeType},x=function(e){return null!=e&&e===e.window},c={type:!0,src:!0,nonce:!0,noModule:!0};function b(e,t,n){var r,i,o=(n=n||E).createElement("script");if(o.text=e,t)for(r in c)(i=t[r]||t.getAttribute&&t.getAttribute(r))&&o.setAttribute(r,i);n.head.appendChild(o).parentNode.removeChild(o)}function w(e){return null==e?e+"":"object"==typeof e||"function"==typeof e?n[o.call(e)]||"object":typeof e}var f="3.4.1",k=function(e,t){return new k.fn.init(e,t)},p=/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g;function d(e){var t=!!e&&"length"in e&&e.length,n=w(e);return!m(e)&&!x(e)&&("array"===n||0===t||"number"==typeof t&&0+~]|"+M+")"+M+"*"),U=new RegExp(M+"|>"),X=new RegExp($),V=new RegExp("^"+I+"$"),G={ID:new RegExp("^#("+I+")"),CLASS:new RegExp("^\\.("+I+")"),TAG:new RegExp("^("+I+"|[*])"),ATTR:new RegExp("^"+W),PSEUDO:new RegExp("^"+$),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+M+"*(even|odd|(([+-]|)(\\d*)n|)"+M+"*(?:([+-]|)"+M+"*(\\d+)|))"+M+"*\\)|)","i"),bool:new RegExp("^(?:"+R+")$","i"),needsContext:new RegExp("^"+M+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+M+"*((?:-\\d)?\\d*)"+M+"*\\)|)(?=[^-]|$)","i")},Y=/HTML$/i,Q=/^(?:input|select|textarea|button)$/i,J=/^h\d$/i,K=/^[^{]+\{\s*\[native \w/,Z=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,ee=/[+~]/,te=new RegExp("\\\\([\\da-f]{1,6}"+M+"?|("+M+")|.)","ig"),ne=function(e,t,n){var r="0x"+t-65536;return r!=r||n?t:r<0?String.fromCharCode(r+65536):String.fromCharCode(r>>10|55296,1023&r|56320)},re=/([\0-\x1f\x7f]|^-?\d)|^-$|[^\0-\x1f\x7f-\uFFFF\w-]/g,ie=function(e,t){return t?"\0"===e?"\ufffd":e.slice(0,-1)+"\\"+e.charCodeAt(e.length-1).toString(16)+" ":"\\"+e},oe=function(){T()},ae=be(function(e){return!0===e.disabled&&"fieldset"===e.nodeName.toLowerCase()},{dir:"parentNode",next:"legend"});try{H.apply(t=O.call(m.childNodes),m.childNodes),t[m.childNodes.length].nodeType}catch(e){H={apply:t.length?function(e,t){L.apply(e,O.call(t))}:function(e,t){var n=e.length,r=0;while(e[n++]=t[r++]);e.length=n-1}}}function se(t,e,n,r){var i,o,a,s,u,l,c,f=e&&e.ownerDocument,p=e?e.nodeType:9;if(n=n||[],"string"!=typeof t||!t||1!==p&&9!==p&&11!==p)return n;if(!r&&((e?e.ownerDocument||e:m)!==C&&T(e),e=e||C,E)){if(11!==p&&(u=Z.exec(t)))if(i=u[1]){if(9===p){if(!(a=e.getElementById(i)))return n;if(a.id===i)return n.push(a),n}else if(f&&(a=f.getElementById(i))&&y(e,a)&&a.id===i)return n.push(a),n}else{if(u[2])return H.apply(n,e.getElementsByTagName(t)),n;if((i=u[3])&&d.getElementsByClassName&&e.getElementsByClassName)return H.apply(n,e.getElementsByClassName(i)),n}if(d.qsa&&!A[t+" "]&&(!v||!v.test(t))&&(1!==p||"object"!==e.nodeName.toLowerCase())){if(c=t,f=e,1===p&&U.test(t)){(s=e.getAttribute("id"))?s=s.replace(re,ie):e.setAttribute("id",s=k),o=(l=h(t)).length;while(o--)l[o]="#"+s+" "+xe(l[o]);c=l.join(","),f=ee.test(t)&&ye(e.parentNode)||e}try{return H.apply(n,f.querySelectorAll(c)),n}catch(e){A(t,!0)}finally{s===k&&e.removeAttribute("id")}}}return g(t.replace(B,"$1"),e,n,r)}function ue(){var r=[];return function e(t,n){return r.push(t+" ")>b.cacheLength&&delete e[r.shift()],e[t+" "]=n}}function le(e){return e[k]=!0,e}function ce(e){var t=C.createElement("fieldset");try{return!!e(t)}catch(e){return!1}finally{t.parentNode&&t.parentNode.removeChild(t),t=null}}function fe(e,t){var n=e.split("|"),r=n.length;while(r--)b.attrHandle[n[r]]=t}function pe(e,t){var n=t&&e,r=n&&1===e.nodeType&&1===t.nodeType&&e.sourceIndex-t.sourceIndex;if(r)return r;if(n)while(n=n.nextSibling)if(n===t)return-1;return e?1:-1}function de(t){return function(e){return"input"===e.nodeName.toLowerCase()&&e.type===t}}function he(n){return function(e){var t=e.nodeName.toLowerCase();return("input"===t||"button"===t)&&e.type===n}}function ge(t){return function(e){return"form"in e?e.parentNode&&!1===e.disabled?"label"in e?"label"in e.parentNode?e.parentNode.disabled===t:e.disabled===t:e.isDisabled===t||e.isDisabled!==!t&&ae(e)===t:e.disabled===t:"label"in e&&e.disabled===t}}function ve(a){return le(function(o){return o=+o,le(function(e,t){var n,r=a([],e.length,o),i=r.length;while(i--)e[n=r[i]]&&(e[n]=!(t[n]=e[n]))})})}function ye(e){return e&&"undefined"!=typeof e.getElementsByTagName&&e}for(e in d=se.support={},i=se.isXML=function(e){var t=e.namespaceURI,n=(e.ownerDocument||e).documentElement;return!Y.test(t||n&&n.nodeName||"HTML")},T=se.setDocument=function(e){var t,n,r=e?e.ownerDocument||e:m;return r!==C&&9===r.nodeType&&r.documentElement&&(a=(C=r).documentElement,E=!i(C),m!==C&&(n=C.defaultView)&&n.top!==n&&(n.addEventListener?n.addEventListener("unload",oe,!1):n.attachEvent&&n.attachEvent("onunload",oe)),d.attributes=ce(function(e){return e.className="i",!e.getAttribute("className")}),d.getElementsByTagName=ce(function(e){return e.appendChild(C.createComment("")),!e.getElementsByTagName("*").length}),d.getElementsByClassName=K.test(C.getElementsByClassName),d.getById=ce(function(e){return a.appendChild(e).id=k,!C.getElementsByName||!C.getElementsByName(k).length}),d.getById?(b.filter.ID=function(e){var t=e.replace(te,ne);return function(e){return e.getAttribute("id")===t}},b.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&E){var n=t.getElementById(e);return n?[n]:[]}}):(b.filter.ID=function(e){var n=e.replace(te,ne);return function(e){var t="undefined"!=typeof e.getAttributeNode&&e.getAttributeNode("id");return t&&t.value===n}},b.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&E){var n,r,i,o=t.getElementById(e);if(o){if((n=o.getAttributeNode("id"))&&n.value===e)return[o];i=t.getElementsByName(e),r=0;while(o=i[r++])if((n=o.getAttributeNode("id"))&&n.value===e)return[o]}return[]}}),b.find.TAG=d.getElementsByTagName?function(e,t){return"undefined"!=typeof t.getElementsByTagName?t.getElementsByTagName(e):d.qsa?t.querySelectorAll(e):void 0}:function(e,t){var n,r=[],i=0,o=t.getElementsByTagName(e);if("*"===e){while(n=o[i++])1===n.nodeType&&r.push(n);return r}return o},b.find.CLASS=d.getElementsByClassName&&function(e,t){if("undefined"!=typeof t.getElementsByClassName&&E)return t.getElementsByClassName(e)},s=[],v=[],(d.qsa=K.test(C.querySelectorAll))&&(ce(function(e){a.appendChild(e).innerHTML="",e.querySelectorAll("[msallowcapture^='']").length&&v.push("[*^$]="+M+"*(?:''|\"\")"),e.querySelectorAll("[selected]").length||v.push("\\["+M+"*(?:value|"+R+")"),e.querySelectorAll("[id~="+k+"-]").length||v.push("~="),e.querySelectorAll(":checked").length||v.push(":checked"),e.querySelectorAll("a#"+k+"+*").length||v.push(".#.+[+~]")}),ce(function(e){e.innerHTML="";var t=C.createElement("input");t.setAttribute("type","hidden"),e.appendChild(t).setAttribute("name","D"),e.querySelectorAll("[name=d]").length&&v.push("name"+M+"*[*^$|!~]?="),2!==e.querySelectorAll(":enabled").length&&v.push(":enabled",":disabled"),a.appendChild(e).disabled=!0,2!==e.querySelectorAll(":disabled").length&&v.push(":enabled",":disabled"),e.querySelectorAll("*,:x"),v.push(",.*:")})),(d.matchesSelector=K.test(c=a.matches||a.webkitMatchesSelector||a.mozMatchesSelector||a.oMatchesSelector||a.msMatchesSelector))&&ce(function(e){d.disconnectedMatch=c.call(e,"*"),c.call(e,"[s!='']:x"),s.push("!=",$)}),v=v.length&&new RegExp(v.join("|")),s=s.length&&new RegExp(s.join("|")),t=K.test(a.compareDocumentPosition),y=t||K.test(a.contains)?function(e,t){var n=9===e.nodeType?e.documentElement:e,r=t&&t.parentNode;return e===r||!(!r||1!==r.nodeType||!(n.contains?n.contains(r):e.compareDocumentPosition&&16&e.compareDocumentPosition(r)))}:function(e,t){if(t)while(t=t.parentNode)if(t===e)return!0;return!1},D=t?function(e,t){if(e===t)return l=!0,0;var n=!e.compareDocumentPosition-!t.compareDocumentPosition;return n||(1&(n=(e.ownerDocument||e)===(t.ownerDocument||t)?e.compareDocumentPosition(t):1)||!d.sortDetached&&t.compareDocumentPosition(e)===n?e===C||e.ownerDocument===m&&y(m,e)?-1:t===C||t.ownerDocument===m&&y(m,t)?1:u?P(u,e)-P(u,t):0:4&n?-1:1)}:function(e,t){if(e===t)return l=!0,0;var n,r=0,i=e.parentNode,o=t.parentNode,a=[e],s=[t];if(!i||!o)return e===C?-1:t===C?1:i?-1:o?1:u?P(u,e)-P(u,t):0;if(i===o)return pe(e,t);n=e;while(n=n.parentNode)a.unshift(n);n=t;while(n=n.parentNode)s.unshift(n);while(a[r]===s[r])r++;return r?pe(a[r],s[r]):a[r]===m?-1:s[r]===m?1:0}),C},se.matches=function(e,t){return se(e,null,null,t)},se.matchesSelector=function(e,t){if((e.ownerDocument||e)!==C&&T(e),d.matchesSelector&&E&&!A[t+" "]&&(!s||!s.test(t))&&(!v||!v.test(t)))try{var n=c.call(e,t);if(n||d.disconnectedMatch||e.document&&11!==e.document.nodeType)return n}catch(e){A(t,!0)}return 0":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(e){return e[1]=e[1].replace(te,ne),e[3]=(e[3]||e[4]||e[5]||"").replace(te,ne),"~="===e[2]&&(e[3]=" "+e[3]+" "),e.slice(0,4)},CHILD:function(e){return e[1]=e[1].toLowerCase(),"nth"===e[1].slice(0,3)?(e[3]||se.error(e[0]),e[4]=+(e[4]?e[5]+(e[6]||1):2*("even"===e[3]||"odd"===e[3])),e[5]=+(e[7]+e[8]||"odd"===e[3])):e[3]&&se.error(e[0]),e},PSEUDO:function(e){var t,n=!e[6]&&e[2];return G.CHILD.test(e[0])?null:(e[3]?e[2]=e[4]||e[5]||"":n&&X.test(n)&&(t=h(n,!0))&&(t=n.indexOf(")",n.length-t)-n.length)&&(e[0]=e[0].slice(0,t),e[2]=n.slice(0,t)),e.slice(0,3))}},filter:{TAG:function(e){var t=e.replace(te,ne).toLowerCase();return"*"===e?function(){return!0}:function(e){return e.nodeName&&e.nodeName.toLowerCase()===t}},CLASS:function(e){var t=p[e+" "];return t||(t=new RegExp("(^|"+M+")"+e+"("+M+"|$)"))&&p(e,function(e){return t.test("string"==typeof e.className&&e.className||"undefined"!=typeof e.getAttribute&&e.getAttribute("class")||"")})},ATTR:function(n,r,i){return function(e){var t=se.attr(e,n);return null==t?"!="===r:!r||(t+="","="===r?t===i:"!="===r?t!==i:"^="===r?i&&0===t.indexOf(i):"*="===r?i&&-1:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i;function j(e,n,r){return m(n)?k.grep(e,function(e,t){return!!n.call(e,t,e)!==r}):n.nodeType?k.grep(e,function(e){return e===n!==r}):"string"!=typeof n?k.grep(e,function(e){return-1)[^>]*|#([\w-]+))$/;(k.fn.init=function(e,t,n){var r,i;if(!e)return this;if(n=n||q,"string"==typeof e){if(!(r="<"===e[0]&&">"===e[e.length-1]&&3<=e.length?[null,e,null]:L.exec(e))||!r[1]&&t)return!t||t.jquery?(t||n).find(e):this.constructor(t).find(e);if(r[1]){if(t=t instanceof k?t[0]:t,k.merge(this,k.parseHTML(r[1],t&&t.nodeType?t.ownerDocument||t:E,!0)),D.test(r[1])&&k.isPlainObject(t))for(r in t)m(this[r])?this[r](t[r]):this.attr(r,t[r]);return this}return(i=E.getElementById(r[2]))&&(this[0]=i,this.length=1),this}return e.nodeType?(this[0]=e,this.length=1,this):m(e)?void 0!==n.ready?n.ready(e):e(k):k.makeArray(e,this)}).prototype=k.fn,q=k(E);var H=/^(?:parents|prev(?:Until|All))/,O={children:!0,contents:!0,next:!0,prev:!0};function P(e,t){while((e=e[t])&&1!==e.nodeType);return e}k.fn.extend({has:function(e){var t=k(e,this),n=t.length;return this.filter(function(){for(var e=0;e\x20\t\r\n\f]*)/i,he=/^$|^module$|\/(?:java|ecma)script/i,ge={option:[1,""],thead:[1,"","
"],col:[2,"","
"],tr:[2,"","
"],td:[3,"","
"],_default:[0,"",""]};function ve(e,t){var n;return n="undefined"!=typeof e.getElementsByTagName?e.getElementsByTagName(t||"*"):"undefined"!=typeof e.querySelectorAll?e.querySelectorAll(t||"*"):[],void 0===t||t&&A(e,t)?k.merge([e],n):n}function ye(e,t){for(var n=0,r=e.length;nx",y.noCloneChecked=!!me.cloneNode(!0).lastChild.defaultValue;var Te=/^key/,Ce=/^(?:mouse|pointer|contextmenu|drag|drop)|click/,Ee=/^([^.]*)(?:\.(.+)|)/;function ke(){return!0}function Se(){return!1}function Ne(e,t){return e===function(){try{return E.activeElement}catch(e){}}()==("focus"===t)}function Ae(e,t,n,r,i,o){var a,s;if("object"==typeof t){for(s in"string"!=typeof n&&(r=r||n,n=void 0),t)Ae(e,s,n,r,t[s],o);return e}if(null==r&&null==i?(i=n,r=n=void 0):null==i&&("string"==typeof n?(i=r,r=void 0):(i=r,r=n,n=void 0)),!1===i)i=Se;else if(!i)return e;return 1===o&&(a=i,(i=function(e){return k().off(e),a.apply(this,arguments)}).guid=a.guid||(a.guid=k.guid++)),e.each(function(){k.event.add(this,t,i,r,n)})}function De(e,i,o){o?(Q.set(e,i,!1),k.event.add(e,i,{namespace:!1,handler:function(e){var t,n,r=Q.get(this,i);if(1&e.isTrigger&&this[i]){if(r.length)(k.event.special[i]||{}).delegateType&&e.stopPropagation();else if(r=s.call(arguments),Q.set(this,i,r),t=o(this,i),this[i](),r!==(n=Q.get(this,i))||t?Q.set(this,i,!1):n={},r!==n)return e.stopImmediatePropagation(),e.preventDefault(),n.value}else r.length&&(Q.set(this,i,{value:k.event.trigger(k.extend(r[0],k.Event.prototype),r.slice(1),this)}),e.stopImmediatePropagation())}})):void 0===Q.get(e,i)&&k.event.add(e,i,ke)}k.event={global:{},add:function(t,e,n,r,i){var o,a,s,u,l,c,f,p,d,h,g,v=Q.get(t);if(v){n.handler&&(n=(o=n).handler,i=o.selector),i&&k.find.matchesSelector(ie,i),n.guid||(n.guid=k.guid++),(u=v.events)||(u=v.events={}),(a=v.handle)||(a=v.handle=function(e){return"undefined"!=typeof k&&k.event.triggered!==e.type?k.event.dispatch.apply(t,arguments):void 0}),l=(e=(e||"").match(R)||[""]).length;while(l--)d=g=(s=Ee.exec(e[l])||[])[1],h=(s[2]||"").split(".").sort(),d&&(f=k.event.special[d]||{},d=(i?f.delegateType:f.bindType)||d,f=k.event.special[d]||{},c=k.extend({type:d,origType:g,data:r,handler:n,guid:n.guid,selector:i,needsContext:i&&k.expr.match.needsContext.test(i),namespace:h.join(".")},o),(p=u[d])||((p=u[d]=[]).delegateCount=0,f.setup&&!1!==f.setup.call(t,r,h,a)||t.addEventListener&&t.addEventListener(d,a)),f.add&&(f.add.call(t,c),c.handler.guid||(c.handler.guid=n.guid)),i?p.splice(p.delegateCount++,0,c):p.push(c),k.event.global[d]=!0)}},remove:function(e,t,n,r,i){var o,a,s,u,l,c,f,p,d,h,g,v=Q.hasData(e)&&Q.get(e);if(v&&(u=v.events)){l=(t=(t||"").match(R)||[""]).length;while(l--)if(d=g=(s=Ee.exec(t[l])||[])[1],h=(s[2]||"").split(".").sort(),d){f=k.event.special[d]||{},p=u[d=(r?f.delegateType:f.bindType)||d]||[],s=s[2]&&new RegExp("(^|\\.)"+h.join("\\.(?:.*\\.|)")+"(\\.|$)"),a=o=p.length;while(o--)c=p[o],!i&&g!==c.origType||n&&n.guid!==c.guid||s&&!s.test(c.namespace)||r&&r!==c.selector&&("**"!==r||!c.selector)||(p.splice(o,1),c.selector&&p.delegateCount--,f.remove&&f.remove.call(e,c));a&&!p.length&&(f.teardown&&!1!==f.teardown.call(e,h,v.handle)||k.removeEvent(e,d,v.handle),delete u[d])}else for(d in u)k.event.remove(e,d+t[l],n,r,!0);k.isEmptyObject(u)&&Q.remove(e,"handle events")}},dispatch:function(e){var t,n,r,i,o,a,s=k.event.fix(e),u=new Array(arguments.length),l=(Q.get(this,"events")||{})[s.type]||[],c=k.event.special[s.type]||{};for(u[0]=s,t=1;t\x20\t\r\n\f]*)[^>]*)\/>/gi,qe=/\s*$/g;function Oe(e,t){return A(e,"table")&&A(11!==t.nodeType?t:t.firstChild,"tr")&&k(e).children("tbody")[0]||e}function Pe(e){return e.type=(null!==e.getAttribute("type"))+"/"+e.type,e}function Re(e){return"true/"===(e.type||"").slice(0,5)?e.type=e.type.slice(5):e.removeAttribute("type"),e}function Me(e,t){var n,r,i,o,a,s,u,l;if(1===t.nodeType){if(Q.hasData(e)&&(o=Q.access(e),a=Q.set(t,o),l=o.events))for(i in delete a.handle,a.events={},l)for(n=0,r=l[i].length;n")},clone:function(e,t,n){var r,i,o,a,s,u,l,c=e.cloneNode(!0),f=oe(e);if(!(y.noCloneChecked||1!==e.nodeType&&11!==e.nodeType||k.isXMLDoc(e)))for(a=ve(c),r=0,i=(o=ve(e)).length;r").attr(n.scriptAttrs||{}).prop({charset:n.scriptCharset,src:n.url}).on("load error",i=function(e){r.remove(),i=null,e&&t("error"===e.type?404:200,e.type)}),E.head.appendChild(r[0])},abort:function(){i&&i()}}});var Vt,Gt=[],Yt=/(=)\?(?=&|$)|\?\?/;k.ajaxSetup({jsonp:"callback",jsonpCallback:function(){var e=Gt.pop()||k.expando+"_"+kt++;return this[e]=!0,e}}),k.ajaxPrefilter("json jsonp",function(e,t,n){var r,i,o,a=!1!==e.jsonp&&(Yt.test(e.url)?"url":"string"==typeof e.data&&0===(e.contentType||"").indexOf("application/x-www-form-urlencoded")&&Yt.test(e.data)&&"data");if(a||"jsonp"===e.dataTypes[0])return r=e.jsonpCallback=m(e.jsonpCallback)?e.jsonpCallback():e.jsonpCallback,a?e[a]=e[a].replace(Yt,"$1"+r):!1!==e.jsonp&&(e.url+=(St.test(e.url)?"&":"?")+e.jsonp+"="+r),e.converters["script json"]=function(){return o||k.error(r+" was not called"),o[0]},e.dataTypes[0]="json",i=C[r],C[r]=function(){o=arguments},n.always(function(){void 0===i?k(C).removeProp(r):C[r]=i,e[r]&&(e.jsonpCallback=t.jsonpCallback,Gt.push(r)),o&&m(i)&&i(o[0]),o=i=void 0}),"script"}),y.createHTMLDocument=((Vt=E.implementation.createHTMLDocument("").body).innerHTML="
",2===Vt.childNodes.length),k.parseHTML=function(e,t,n){return"string"!=typeof e?[]:("boolean"==typeof t&&(n=t,t=!1),t||(y.createHTMLDocument?((r=(t=E.implementation.createHTMLDocument("")).createElement("base")).href=E.location.href,t.head.appendChild(r)):t=E),o=!n&&[],(i=D.exec(e))?[t.createElement(i[1])]:(i=we([e],t,o),o&&o.length&&k(o).remove(),k.merge([],i.childNodes)));var r,i,o},k.fn.load=function(e,t,n){var r,i,o,a=this,s=e.indexOf(" ");return-1").append(k.parseHTML(e)).find(r):e)}).always(n&&function(e,t){a.each(function(){n.apply(this,o||[e.responseText,t,e])})}),this},k.each(["ajaxStart","ajaxStop","ajaxComplete","ajaxError","ajaxSuccess","ajaxSend"],function(e,t){k.fn[t]=function(e){return this.on(t,e)}}),k.expr.pseudos.animated=function(t){return k.grep(k.timers,function(e){return t===e.elem}).length},k.offset={setOffset:function(e,t,n){var r,i,o,a,s,u,l=k.css(e,"position"),c=k(e),f={};"static"===l&&(e.style.position="relative"),s=c.offset(),o=k.css(e,"top"),u=k.css(e,"left"),("absolute"===l||"fixed"===l)&&-1<(o+u).indexOf("auto")?(a=(r=c.position()).top,i=r.left):(a=parseFloat(o)||0,i=parseFloat(u)||0),m(t)&&(t=t.call(e,n,k.extend({},s))),null!=t.top&&(f.top=t.top-s.top+a),null!=t.left&&(f.left=t.left-s.left+i),"using"in t?t.using.call(e,f):c.css(f)}},k.fn.extend({offset:function(t){if(arguments.length)return void 0===t?this:this.each(function(e){k.offset.setOffset(this,t,e)});var e,n,r=this[0];return r?r.getClientRects().length?(e=r.getBoundingClientRect(),n=r.ownerDocument.defaultView,{top:e.top+n.pageYOffset,left:e.left+n.pageXOffset}):{top:0,left:0}:void 0},position:function(){if(this[0]){var e,t,n,r=this[0],i={top:0,left:0};if("fixed"===k.css(r,"position"))t=r.getBoundingClientRect();else{t=this.offset(),n=r.ownerDocument,e=r.offsetParent||n.documentElement;while(e&&(e===n.body||e===n.documentElement)&&"static"===k.css(e,"position"))e=e.parentNode;e&&e!==r&&1===e.nodeType&&((i=k(e).offset()).top+=k.css(e,"borderTopWidth",!0),i.left+=k.css(e,"borderLeftWidth",!0))}return{top:t.top-i.top-k.css(r,"marginTop",!0),left:t.left-i.left-k.css(r,"marginLeft",!0)}}},offsetParent:function(){return this.map(function(){var e=this.offsetParent;while(e&&"static"===k.css(e,"position"))e=e.offsetParent;return e||ie})}}),k.each({scrollLeft:"pageXOffset",scrollTop:"pageYOffset"},function(t,i){var o="pageYOffset"===i;k.fn[t]=function(e){return _(this,function(e,t,n){var r;if(x(e)?r=e:9===e.nodeType&&(r=e.defaultView),void 0===n)return r?r[i]:e[t];r?r.scrollTo(o?r.pageXOffset:n,o?n:r.pageYOffset):e[t]=n},t,e,arguments.length)}}),k.each(["top","left"],function(e,n){k.cssHooks[n]=ze(y.pixelPosition,function(e,t){if(t)return t=_e(e,n),$e.test(t)?k(e).position()[n]+"px":t})}),k.each({Height:"height",Width:"width"},function(a,s){k.each({padding:"inner"+a,content:s,"":"outer"+a},function(r,o){k.fn[o]=function(e,t){var n=arguments.length&&(r||"boolean"!=typeof e),i=r||(!0===e||!0===t?"margin":"border");return _(this,function(e,t,n){var r;return x(e)?0===o.indexOf("outer")?e["inner"+a]:e.document.documentElement["client"+a]:9===e.nodeType?(r=e.documentElement,Math.max(e.body["scroll"+a],r["scroll"+a],e.body["offset"+a],r["offset"+a],r["client"+a])):void 0===n?k.css(e,t,i):k.style(e,t,n,i)},s,n?e:void 0,n)}})}),k.each("blur focus focusin focusout resize scroll click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup contextmenu".split(" "),function(e,n){k.fn[n]=function(e,t){return 0 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | FlaskApp 10 | 11 | 12 | 13 |

Flask Desktop Application

14 | 15 | 16 | 17 | Go home 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /examples/flaskapp/van.py: -------------------------------------------------------------------------------- 1 | import pyvan 2 | 3 | 4 | OPTIONS = { 5 | "main_file_name": "main.py", 6 | "show_console": False, 7 | "use_pipreqs": True 8 | } 9 | 10 | 11 | pyvan.build(**OPTIONS) 12 | -------------------------------------------------------------------------------- /pyvan.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ClimenteA/pyvan/12e0a677e1099414ede737a523e37093877c9008/pyvan.png -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | 3 | # python setup.py bdist_wheel sdist 4 | # cd dist 5 | # twine upload * 6 | 7 | 8 | with open("README.md", "r") as fh: 9 | long_description = fh.read() 10 | 11 | 12 | setup( 13 | name="pyvan", 14 | version="1.2.3", 15 | description="Make runnable desktop apps from your python scripts more easily with pyvan!", 16 | url="https://github.com/ClimenteA/pyvan", 17 | author="Climente Alin", 18 | author_email="climente.alin@gmail.com", 19 | license="MIT", 20 | py_modules=["pyvan"], 21 | install_requires=["pipreqs", "click", "requests", "gen-exe"], 22 | packages=find_packages(), 23 | long_description=long_description, 24 | long_description_content_type="text/markdown", 25 | package_dir={"": "src"}, 26 | entry_points={"console_scripts": ["pyvan=pyvan:cli"]}, 27 | ) 28 | -------------------------------------------------------------------------------- /src/pyvan.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import re 3 | import time 4 | import os 5 | import sys 6 | import shutil 7 | import zipfile 8 | import subprocess 9 | import click 10 | import requests 11 | from pathlib import Path 12 | from genexe.generate_exe import generate_exe 13 | 14 | 15 | # python_version can be anything of the form: `x.x.x` where any x may be set to a positive integer. 16 | PYTHON_VERSION_REGEX = re.compile(r"^(\d+|x)\.(\d+|x)\.(\d+|x)$") 17 | GET_PIP_URL = "https://bootstrap.pypa.io/get-pip.py" 18 | PYTHON_URL = "https://www.python.org/ftp/python" 19 | HEADER_NO_CONSOLE = """import sys, os 20 | if sys.executable.endswith('pythonw.exe'): 21 | sys.stdout = open(os.devnull, 'w') 22 | sys.stderr = open(os.path.join(os.getenv(\'TEMP\'), \'stderr-{}\'.format(os.path.basename(sys.argv[0]))), "w") 23 | 24 | """ 25 | 26 | 27 | def log(message: str, log_type: str, verbose: bool = True): 28 | if verbose is True: 29 | print(message) 30 | elif verbose is False and log_type in ["warning", "error"]: 31 | print(message) 32 | 33 | 34 | def execute_os_command(command, cwd=None, verbose=True): 35 | """Execute terminal command""" 36 | 37 | log(f"Running command: {command}", "info", verbose) 38 | process = subprocess.Popen( 39 | command, 40 | shell=True, 41 | stdout=subprocess.PIPE, 42 | stderr=subprocess.STDOUT, 43 | cwd=os.getcwd() if cwd is None else cwd, 44 | ) 45 | 46 | # Poll process for new output until finished 47 | while True: 48 | nextline = process.stdout.readline().decode("UTF-8") 49 | if nextline == "" and process.poll() is not None: 50 | break 51 | sys.stdout.write(nextline) 52 | sys.stdout.flush() 53 | 54 | output = process.communicate()[0] 55 | exit_code = process.returncode 56 | 57 | if exit_code == 0: 58 | log(output, "info", verbose) 59 | return output 60 | else: 61 | raise Exception(command, exit_code, output) 62 | 63 | 64 | def put_code_in_dist_folder(source_dir, target_dir, build_dir, verbose): 65 | """Copy .py files and others to target folder""" 66 | if not os.path.isdir(os.path.dirname(target_dir)): 67 | os.makedirs(os.path.dirname(target_dir)) 68 | log(f"Copying files from {source_dir} to {target_dir}!", "info", verbose) 69 | shutil.copytree( 70 | src=source_dir, 71 | dst=target_dir, 72 | ignore=shutil.ignore_patterns( 73 | os.path.basename(build_dir), "__pycache__", "*.pyc" 74 | ), 75 | dirs_exist_ok=True, 76 | ) 77 | log("Files copied!", "info", verbose) 78 | 79 | 80 | def prep_requirements(use_pipreqs, target_req_file, input_dir, build_dir, verbose): 81 | """Create requirements.txt file from which to install modules on embeded python version""" 82 | 83 | if use_pipreqs: 84 | log("Searching modules needed using 'pipreqs'...", "info", verbose) 85 | execute_os_command( 86 | command=f"pipreqs {input_dir} --force --ignore {os.path.basename(build_dir)} --savepath {target_req_file}", 87 | verbose=verbose, 88 | ) 89 | log("Done!", "info", verbose) 90 | else: 91 | log("Searching modules needed using 'pip freeze'...", "info", verbose) 92 | execute_os_command( 93 | command=f"pip3.exe freeze > {target_req_file}", 94 | cwd=input_dir, 95 | verbose=verbose, 96 | ) 97 | log("Done!", "info", verbose) 98 | 99 | 100 | def filter_requirements(target_req_file, include_modules, exclude_modules, verbose): 101 | """Filter modules and keep only the ones needed""" 102 | 103 | log("Checking which modules to exclude or to keep", "info", verbose) 104 | with open(target_req_file, "r") as r: 105 | modules_to_install = r.read().splitlines() 106 | 107 | if any(exclude_modules): 108 | modules_to_install = list( 109 | set.difference(set(modules_to_install), set(exclude_modules)) 110 | ) 111 | 112 | if any(include_modules): 113 | modules_to_install = modules_to_install + include_modules 114 | 115 | log(f"Updating {target_req_file} file", "info", verbose) 116 | with open(target_req_file, "w") as f: 117 | f.write("\n".join(modules_to_install)) 118 | 119 | log(f"File {target_req_file} done!", "info", verbose) 120 | 121 | 122 | def add_embeded_and_pip_to_dist( 123 | get_pip_file, embedded_python_file, pydist_dir, verbose 124 | ): 125 | """Copy embeded python and get-pip file to dist folder""" 126 | 127 | log(f"Extracting {embedded_python_file} to {pydist_dir} folder", "info", verbose) 128 | zip_ref = zipfile.ZipFile(embedded_python_file, "r") 129 | zip_ref.extractall(pydist_dir) 130 | zip_ref.close() 131 | log("Zip file extracted!", "info", verbose) 132 | 133 | shutil.copy2(get_pip_file, pydist_dir) 134 | log(f"File {get_pip_file} file copied to {pydist_dir}!", "info", verbose) 135 | 136 | 137 | def prepare_for_pip_install( 138 | pth_file, zip_pyfile, pydist_sub_dir_str, source_sub_dir_str, verbose 139 | ): 140 | """ 141 | Prepare the extracted embedded python version for pip installation 142 | - Uncommented 'import site' line from pythonXX._pth file 143 | - Extract pythonXX.zip zip file to pythonXX.zip folder and delete pythonXX.zip zip file 144 | """ 145 | log( 146 | f"Generated '{pth_file}' file with uncommented 'import site' line.", 147 | "info", 148 | verbose, 149 | ) 150 | with open(pth_file, "w") as f: 151 | rel_path_to_sources = ( 152 | "." if pydist_sub_dir_str == "" else ".." 153 | ) + source_sub_dir_str 154 | f.write( 155 | f"{os.path.basename(zip_pyfile)}\n{rel_path_to_sources}\n\n# Uncomment to run site.main() automatically\nimport site\n" 156 | ) 157 | 158 | log(f"Extracting {zip_pyfile} file", "info", verbose) 159 | 160 | temp_folder = str(zip_pyfile + "_temp") 161 | os.mkdir(temp_folder) 162 | 163 | zip_ref = zipfile.ZipFile(zip_pyfile, "r") 164 | zip_ref.extractall(temp_folder) 165 | zip_ref.close() 166 | 167 | os.remove(zip_pyfile) 168 | 169 | for _ in range(10): 170 | try: 171 | # Try 10 times to delete the file 172 | os.rename(temp_folder, zip_pyfile) 173 | except: # Permision error 174 | time.sleep(0.3) 175 | 176 | log(f"Zip file extracted to {zip_pyfile} folder!", "info", verbose) 177 | 178 | 179 | def install_requirements( 180 | pydist_dir, build_dir, req_file, extra_pip_install_args=None, verbose=True 181 | ): 182 | """ 183 | Install pip and the modules from requirements.txt file 184 | - extra_pip_install_args (optional `List[str]`) : pass these additional arguments to the pip install command 185 | """ 186 | log("Installing pip..", "info", verbose) 187 | 188 | execute_os_command( 189 | command="python.exe get-pip.py --no-warn-script-location", 190 | cwd=pydist_dir, 191 | verbose=verbose, 192 | ) 193 | 194 | if not os.path.isdir(os.path.join(pydist_dir, "Scripts")): 195 | raise Exception("Module 'pip' didn't install corectly from 'get-pip.py' file!") 196 | 197 | log("Module pip installed!", "info", verbose) 198 | 199 | scripts_dir = os.path.join(pydist_dir, "Scripts") 200 | 201 | if extra_pip_install_args is not None: 202 | extra_args_str = " " + " ".join(extra_pip_install_args) 203 | else: 204 | extra_args_str = "" 205 | 206 | try: 207 | cmd = f"pip3.exe install --no-cache-dir --no-warn-script-location -r {req_file}{extra_args_str}" 208 | execute_os_command(command=cmd, cwd=scripts_dir, verbose=verbose) 209 | except Exception as err: 210 | log(f"{err}\nInstalling modules one by one..", "warning", verbose) 211 | 212 | with open(req_file, "r") as f: 213 | modules = f.read().splitlines() 214 | 215 | for module in modules: 216 | try: 217 | cmd = f"pip3.exe install --no-cache-dir --no-warn-script-location {module}{extra_args_str}" 218 | execute_os_command(command=cmd, cwd=scripts_dir, verbose=verbose) 219 | except Exception as err: 220 | log(f"{err}\nFAILED TO INSTALL {module}", "error", verbose) 221 | with open( 222 | os.path.join(build_dir, "FAILED_TO_INSTALL_MODULES.txt"), "a" 223 | ) as f: 224 | f.write(str(module + "\n")) 225 | 226 | 227 | def make_startup_exe( 228 | main_file_name, 229 | show_console, 230 | build_dir, 231 | relative_pydist_dir, 232 | relative_source_dir, 233 | icon_file=None, 234 | verbose=True, 235 | ): 236 | """Make the startup exe file needed to run the script""" 237 | log("Making startup exe file", "info", verbose) 238 | exe_fname = os.path.join(build_dir, main_file_name.split(".py")[0] + ".exe") 239 | python_entrypoint = "python.exe" 240 | command_str = f'""{{EXE_DIR}}\\{relative_pydist_dir}\\{python_entrypoint}" "{{EXE_DIR}}\\{relative_source_dir}\\{main_file_name}""' 241 | generate_exe( 242 | target=Path(exe_fname), 243 | command=command_str, 244 | icon_file=None if icon_file is None else Path(icon_file), 245 | show_console=show_console, 246 | ) 247 | 248 | if not show_console: 249 | with open(main_file_name, "r", encoding="utf8", errors="surrogateescape") as f: 250 | main_content = f.read() 251 | if HEADER_NO_CONSOLE not in main_content: 252 | with open( 253 | main_file_name, "w", encoding="utf8", errors="surrogateescape" 254 | ) as f: 255 | f.write(str(HEADER_NO_CONSOLE + main_content)) 256 | 257 | log("Done!", "info", verbose) 258 | 259 | 260 | def download_url(url, save_path, chunk_size=128): 261 | """Download streaming a file url to save_path""" 262 | 263 | r = requests.get(url, stream=True) 264 | with open(save_path, "wb") as fd: 265 | for chunk in r.iter_content(chunk_size=chunk_size): 266 | fd.write(chunk) 267 | 268 | 269 | def get_all_available_python_versions(): 270 | r = requests.get("https://www.python.org/ftp/python/") 271 | result = [ 272 | tuple([int(e) for e in v.split(".")]) 273 | for v in re.findall(r">(\d+\.\d+\.\d+)/<", r.text) 274 | ] 275 | return [v for v in sorted(result)] # lowest to highest 276 | 277 | 278 | def resolve_python_version(python_version): 279 | """ 280 | Based on a python_version string resolve all the unknowns 281 | python_version (str) : 282 | can be None or of the form `x.x.x` where x may be an positive integer 283 | This method will attempt to resolve all the x's to the highest possible numbers. 284 | 285 | Note: In the case None is passed as the input 286 | the highest version of Python before the last minor release will be used. 287 | 288 | """ 289 | 290 | if python_version is not None: 291 | if not re.match(PYTHON_VERSION_REGEX, python_version): 292 | raise ValueError( 293 | "Specified python_version does not have the correct format, it should be of format: `x.x.x` where x can be replaced with a positive number." 294 | ) 295 | version_strs = python_version.split(".") 296 | needs_resolving = any([e == "x" for e in version_strs]) 297 | if not needs_resolving: 298 | return tuple(map(int, version_strs)) 299 | # all other options need resolving 300 | all_py_versions = get_all_available_python_versions() 301 | if len(all_py_versions) == 0: 302 | raise RuntimeError( 303 | "All available Python versions returned an empty list, this should not happen!" 304 | ) 305 | if python_version is None: 306 | max_py_version = all_py_versions[-1] 307 | py_versions = [ 308 | v for v in all_py_versions if max_py_version[1] - 1 == v[1] 309 | ] # pick candidates one minor version less than the max 310 | return py_versions[-1] 311 | else: 312 | py_versions = all_py_versions 313 | for i, e in enumerate(python_version.split(".")): 314 | if e != "x": 315 | py_versions = [v for v in py_versions if v[i] == int(e)] 316 | if len(py_versions) > 0: 317 | return py_versions[-1] 318 | else: 319 | raise ValueError( 320 | f"Python version: {python_version} does not exists within the available Python versions list." 321 | ) 322 | 323 | 324 | def find_or_download_required_install_files( 325 | path_to_get_pip_and_python_embedded_zip, python_version, verbose 326 | ): 327 | # Get the path to python embedded zip file and get-pip.py file 328 | if path_to_get_pip_and_python_embedded_zip == "": 329 | files_path = os.path.join(os.getenv("USERPROFILE"), "Downloads") 330 | else: 331 | files_path = path_to_get_pip_and_python_embedded_zip 332 | 333 | get_pip_path = os.path.join(files_path, "get-pip.py") 334 | if "get-pip.py" not in os.listdir(files_path): 335 | log( 336 | f"'get-pip.py' not found in {files_path}, attempting to download it...", 337 | "info", 338 | verbose, 339 | ) 340 | download_url(url=GET_PIP_URL, save_path=get_pip_path) 341 | if not os.path.isfile(get_pip_path): 342 | raise RuntimeError( 343 | f"Could not find get-pip.py in folder: {files_path}, and the download failed..." 344 | ) 345 | 346 | resolved_python_version = resolve_python_version(python_version=python_version) 347 | log( 348 | f"Resolved python_version {python_version}: {resolved_python_version}", 349 | "info", 350 | verbose, 351 | ) 352 | 353 | python_version_str = "{v[0]}.{v[1]}.{v[2]}".format(v=resolved_python_version) 354 | embedded_file_name = f"python-{python_version_str}-embed-amd64.zip" 355 | embedded_path_file = os.path.join(files_path, embedded_file_name) 356 | if not os.path.isfile(embedded_path_file): 357 | log( 358 | f"{embedded_file_name} not found int {files_path}, attempting to download it.", 359 | "info", 360 | verbose, 361 | ) 362 | download_url( 363 | url=f"{PYTHON_URL}/{python_version_str}/{embedded_file_name}", 364 | save_path=embedded_path_file, 365 | ) 366 | if not os.path.isfile(embedded_path_file): 367 | raise RuntimeError( 368 | f"Could not find {embedded_file_name} in folder: {files_path}, and the download failed..." 369 | ) 370 | 371 | short_python_version_str = "python" + "{v[0]}.{v[1]}".format( 372 | v=resolved_python_version 373 | ).replace(".", "") 374 | pth_file = short_python_version_str + "._pth" 375 | zip_pyfile = short_python_version_str + ".zip" 376 | 377 | log( 378 | f"Using Python-{python_version_str} from:\n {get_pip_path} \n {embedded_path_file}", 379 | "info", 380 | verbose, 381 | ) 382 | 383 | return get_pip_path, embedded_path_file, pth_file, zip_pyfile 384 | 385 | 386 | def display_pyvan_build_config( 387 | input_dir, 388 | build_dir, 389 | exclude_modules, 390 | extra_pip_install_args, 391 | include_modules, 392 | install_only_these_modules, 393 | main_file_name, 394 | pydist_sub_dir, 395 | show_console, 396 | source_sub_dir, 397 | use_existing_requirements, 398 | use_pipreqs, 399 | python_version, 400 | icon_file, 401 | verbose, 402 | ): 403 | log("===PYVAN BUILD CONFIGURATION===", "info", verbose) 404 | log(f"Input dir: {input_dir}", "info", verbose) 405 | log(f"Build dir: {build_dir}", "info", verbose) 406 | log(f"Python distribution will be installed in: {pydist_sub_dir}", "info", verbose) 407 | log(f"App source code will be installed in: {source_sub_dir}", "info", verbose) 408 | log("===REQUIREMENTS===", "info", verbose) 409 | if use_existing_requirements: 410 | log( 411 | f"pyvan will try to install from existing requirements.txt at {input_dir}", 412 | "info", 413 | verbose, 414 | ) 415 | elif any(install_only_these_modules): 416 | log( 417 | "pyvan will generate a requirements.txt for you based on the following specified modules:", 418 | "info", 419 | verbose, 420 | ) 421 | log( 422 | f"install_only_these_modules: {install_only_these_modules}", 423 | "info", 424 | verbose, 425 | ) 426 | else: 427 | log( 428 | "pyvan will try to resolve requirements for you using pipreqs and/or pip freeze:", 429 | "info", 430 | verbose, 431 | ) 432 | log(f"use_pip_reqs: {use_pipreqs}", "info", verbose) 433 | log(f"include_modules: {include_modules}", "info", verbose) 434 | log(f"exclude_modules: {exclude_modules}", "info", verbose) 435 | log("===BUILD OPTIONS===", "info", verbose) 436 | if python_version is not None: 437 | log( 438 | f"pyvan will attempt to install python version: {python_version}", 439 | "info", 440 | verbose, 441 | ) 442 | else: 443 | log( 444 | "no python version specified - pyvan will attempt to install latest stable python version", 445 | "info", 446 | verbose, 447 | ) 448 | log( 449 | f"requirements will be installed with{'' if any(extra_pip_install_args) else 'out'} additional pip arguments", 450 | "info", 451 | verbose, 452 | ) 453 | if any(extra_pip_install_args): 454 | log(f"extra_pip_install_args: {extra_pip_install_args}", "info", verbose) 455 | log("===EXE FILE===", "info", verbose) 456 | log(f"pyvan will generate an exe file for you in {build_dir}", "info", verbose) 457 | log("pyvan will use the following settings:", "info", verbose) 458 | log(f"main_file_name: {main_file_name}", "info", verbose) 459 | log(f"show_console: {show_console}", "info", verbose) 460 | if icon_file is not None: 461 | log(f"icon_file: {icon_file}", "info", verbose) 462 | else: 463 | log("no icon file was set.", "info", verbose) 464 | log("\n===START PYVAN BUILD===", "info", verbose) 465 | 466 | 467 | def prepare_empty_build_dir(build_dir, verbose): 468 | # Delete build folder if it exists 469 | if os.path.isdir(build_dir): 470 | log( 471 | f"Existing build directory found, removing contents... {build_dir}", 472 | "info", 473 | verbose, 474 | ) 475 | shutil.rmtree(build_dir) 476 | os.makedirs(build_dir) 477 | 478 | 479 | def prepare_build_requirements_file( 480 | input_dir, 481 | build_dir, 482 | build_req_file, 483 | use_existing_requirements, 484 | exclude_modules, 485 | install_only_these_modules, 486 | include_modules, 487 | use_pipreqs, 488 | verbose, 489 | ): 490 | base_dir_req_file = os.path.join(input_dir, "requirements.txt") 491 | if use_existing_requirements: 492 | if not os.path.isfile(base_dir_req_file): 493 | raise FileNotFoundError( 494 | f"No requirements.txt file was found in: {input_dir}\nuse_existing_requirements requires one." 495 | ) 496 | log( 497 | f"Using/copying existing requirements.txt file from: {input_dir}", 498 | "info", 499 | verbose, 500 | ) 501 | shutil.copy(src=base_dir_req_file, dst=build_req_file) 502 | elif not any(install_only_these_modules): 503 | try: 504 | prep_requirements( 505 | use_pipreqs=use_pipreqs, 506 | target_req_file=build_req_file, 507 | input_dir=input_dir, 508 | build_dir=build_dir, 509 | verbose=verbose, 510 | ) 511 | except: 512 | failed = not use_pipreqs 513 | if not failed: 514 | try: 515 | prep_requirements( 516 | use_pipreqs=False, 517 | target_req_file=build_req_file, 518 | input_dir=input_dir, 519 | build_dir=build_dir, 520 | verbose=verbose, 521 | ) 522 | except: 523 | failed = True 524 | if failed: 525 | raise RuntimeError( 526 | "pyvan was unable to generate a requirements.txt. Please add modules needed in OPTIONS['include_modules'] or provide a requirements.txt file and specify OPTIONS['use_existing_requirements']!" 527 | ) 528 | 529 | filter_requirements( 530 | target_req_file=build_req_file, 531 | include_modules=include_modules, 532 | exclude_modules=exclude_modules, 533 | verbose=verbose, 534 | ) 535 | else: 536 | with open(build_req_file, "w") as f: 537 | f.write("\n".join(install_only_these_modules)) 538 | 539 | 540 | def build( 541 | main_file_name, 542 | show_console=False, 543 | input_dir=os.getcwd(), 544 | build_dir=os.path.join(os.getcwd(), "dist"), 545 | pydist_sub_dir="pydist", 546 | source_sub_dir="", 547 | python_version=None, 548 | use_pipreqs=True, 549 | include_modules=(), 550 | exclude_modules=(), 551 | install_only_these_modules=(), 552 | use_existing_requirements=False, 553 | extra_pip_install_args=(), 554 | path_to_get_pip_and_python_embedded_zip="", 555 | icon_file=None, 556 | verbose=True, 557 | ): 558 | """Calling all funcs needed and processing options""" 559 | if isinstance(main_file_name, dict): 560 | raise ValueError( 561 | "Old interface was passed to `pyvan.build`, please " 562 | "dereference the options dictionary using: `pyvan.build(**OPTIONS)`" 563 | ) 564 | input_dir = os.path.abspath(input_dir) 565 | build_dir = os.path.abspath(build_dir) 566 | pydist_sub_dir = ( 567 | build_dir if pydist_sub_dir == "" else os.path.join(build_dir, pydist_sub_dir) 568 | ) 569 | source_sub_dir = ( 570 | build_dir if source_sub_dir == "" else os.path.join(build_dir, source_sub_dir) 571 | ) 572 | build_req_file = os.path.join(build_dir, "requirements.txt") 573 | 574 | display_pyvan_build_config( 575 | input_dir, 576 | build_dir, 577 | exclude_modules, 578 | extra_pip_install_args, 579 | include_modules, 580 | install_only_these_modules, 581 | main_file_name, 582 | pydist_sub_dir, 583 | show_console, 584 | source_sub_dir, 585 | use_existing_requirements, 586 | use_pipreqs, 587 | python_version, 588 | icon_file, 589 | verbose, 590 | ) 591 | GET_PIP_PATH, PYTHON_EMBEDED_PATH, pth_file, zip_pyfile = ( 592 | find_or_download_required_install_files( 593 | path_to_get_pip_and_python_embedded_zip=path_to_get_pip_and_python_embedded_zip, 594 | python_version=python_version, 595 | verbose=verbose, 596 | ) 597 | ) 598 | prepare_empty_build_dir(build_dir, verbose) 599 | prepare_build_requirements_file( 600 | input_dir=input_dir, 601 | build_dir=build_dir, 602 | build_req_file=build_req_file, 603 | use_existing_requirements=use_existing_requirements, 604 | use_pipreqs=use_pipreqs, 605 | exclude_modules=exclude_modules, 606 | include_modules=include_modules, 607 | install_only_these_modules=install_only_these_modules, 608 | verbose=verbose, 609 | ) 610 | put_code_in_dist_folder( 611 | source_dir=input_dir, 612 | target_dir=source_sub_dir, 613 | build_dir=build_dir, 614 | verbose=verbose, 615 | ) 616 | add_embeded_and_pip_to_dist( 617 | get_pip_file=GET_PIP_PATH, 618 | embedded_python_file=PYTHON_EMBEDED_PATH, 619 | pydist_dir=pydist_sub_dir, 620 | verbose=verbose, 621 | ) 622 | make_startup_exe( 623 | main_file_name=main_file_name, 624 | show_console=show_console, 625 | build_dir=build_dir, 626 | relative_pydist_dir="" 627 | if pydist_sub_dir == build_dir 628 | else pydist_sub_dir.replace(build_dir, "") + "\\", 629 | relative_source_dir="" 630 | if source_sub_dir == build_dir 631 | else source_sub_dir.replace(build_dir, "") + "\\", 632 | icon_file=icon_file, 633 | verbose=verbose, 634 | ) 635 | prepare_for_pip_install( 636 | pth_file=os.path.join(pydist_sub_dir, pth_file), 637 | zip_pyfile=os.path.join(pydist_sub_dir, zip_pyfile), 638 | pydist_sub_dir_str=pydist_sub_dir.replace(build_dir, ""), 639 | source_sub_dir_str=source_sub_dir.replace(build_dir, ""), 640 | verbose=verbose, 641 | ) 642 | install_requirements( 643 | pydist_dir=pydist_sub_dir, 644 | build_dir=build_dir, 645 | req_file=build_req_file, 646 | extra_pip_install_args=extra_pip_install_args, 647 | verbose=verbose, 648 | ) 649 | 650 | log( 651 | f"\n\nFinished! Folder '{build_dir}' contains your runnable application!\n\n", 652 | "info", 653 | verbose, 654 | ) 655 | log("===END PYVAN BUILD===", "info", verbose) 656 | 657 | 658 | def validate_python_version_input(ctx, param, value): 659 | if value is None: 660 | return None 661 | if re.match(PYTHON_VERSION_REGEX, value): 662 | return value 663 | else: 664 | raise click.BadParameter( 665 | "Python version must be of format: `x.x.x` where x may be a positive integer." 666 | ) 667 | 668 | 669 | @click.command(name="cli") 670 | @click.argument("main_file_name", type=click.Path(exists=False, dir_okay=False)) 671 | @click.option( 672 | "--no-console", 673 | "-nc", 674 | "show_console", 675 | is_flag=True, 676 | default=True, 677 | help="Specify to hide the console window when running the application, e.g. for a service or GUI app", 678 | ) 679 | @click.option( 680 | "--use-existing-reqs", 681 | "use_existing_requirements", 682 | is_flag=True, 683 | default=False, 684 | help="Specify to use an exsiting requirements.txt in the `input_dir` instead of trying to resolve the requirements automatically. Default: try to resolve requirements.", 685 | ) 686 | @click.option( 687 | "--no-pipreqs", 688 | "use_pipreqs", 689 | is_flag=True, 690 | default=True, 691 | help="Specify to skip using pipreqs for resolving the requirements.txt file. Default: use pipreqs.", 692 | ) 693 | @click.option( 694 | "--python-version", 695 | "-py", 696 | "python_version", 697 | type=str, 698 | default=None, 699 | help="Specify to fix the embedded python version number, format x.x.x with x a positive integer. Default: use highest available stable python version.", 700 | callback=validate_python_version_input, 701 | ) 702 | @click.option( 703 | "--input-dir", 704 | default=os.path.abspath(os.getcwd()), 705 | type=click.Path(exists=True, dir_okay=True, file_okay=False, resolve_path=True), 706 | help="The directory with the `main_file_name` file and other files to install. Default: the current working directory.", 707 | ) 708 | @click.option( 709 | "--build-dir", 710 | default=os.path.abspath(os.path.join(os.getcwd(), "dist")), 711 | type=click.Path(exists=False, dir_okay=True, file_okay=False, resolve_path=True), 712 | help="The directory in which pyvan will create the stand-alone distribution. Default: ./dist", 713 | ) 714 | @click.option( 715 | "--pydist-sub-dir", 716 | "pydist_sub_dir", 717 | default="pydist", 718 | type=click.Path(exists=False), 719 | help="A sub directory relative to `build_dir` where the stand-alone python distribution will be installed. Default: ./pydist", 720 | ) 721 | @click.option( 722 | "--source-sub-dir", 723 | "source_sub_dir", 724 | default="", 725 | type=click.Path(exists=False), 726 | help="A sub directory relative to `build_dir` where the to execute python files will be installed. Default: `build_dir`", 727 | ) 728 | @click.option( 729 | "--embedded-files-dir", 730 | "path_to_get_pip_and_python_embedded_zip", 731 | default=None, 732 | type=click.Path(exists=True, dir_okay=True, file_okay=False, resolve_path=True), 733 | help="The directory which should contain 'get-pip.py' and the 'python-x.x.x-embed-amdxx.zip' files. Default: is the users Download directory.", 734 | ) 735 | @click.option( 736 | "--req-install-only", 737 | "-r", 738 | "install_only_these_modules", 739 | default=(), 740 | multiple=True, 741 | type=str, 742 | help="Specify these to directly generate a requirements.txt file using the specified modules. Default: [], use pipreqs.", 743 | ) 744 | @click.option( 745 | "--req-include", 746 | "-i", 747 | "include_modules", 748 | default=(), 749 | multiple=True, 750 | type=str, 751 | help="Specify these to directly add additional modules to a generated requirements.txt file. Default: [].", 752 | ) 753 | @click.option( 754 | "--req-exclude", 755 | "-e", 756 | "exclude_modules", 757 | default=(), 758 | multiple=True, 759 | type=str, 760 | help="Specify these to directly remove modules from a generated requirements.txt file. Default: [].", 761 | ) 762 | @click.option( 763 | "--pip-install-arg", 764 | "-a", 765 | "extra_pip_install_args", 766 | default=(), 767 | multiple=True, 768 | type=str, 769 | help="These arguments will be added to the pip install command during the stand-alone distribution build and allow the user to specify additional arguments this way. Default: [].", 770 | ) 771 | @click.option( 772 | "--icon-file", 773 | "--icon", 774 | "icon_file", 775 | default=None, 776 | type=click.Path(exists=True, dir_okay=False, file_okay=True, resolve_path=True), 777 | help="An optional icon file to add to the generated executable for the stand-alone distribution. Default: don't use an icon", 778 | ) 779 | @click.option( 780 | "--verbose", 781 | "verbose", 782 | is_flag=True, 783 | default=True, 784 | help="Limit the amount of logs you see in the terminal. Show only warnings or errors.", 785 | ) 786 | def cli( 787 | main_file_name, 788 | show_console, 789 | input_dir, 790 | build_dir, 791 | pydist_sub_dir, 792 | source_sub_dir, 793 | use_pipreqs, 794 | python_version, 795 | include_modules, 796 | exclude_modules, 797 | install_only_these_modules, 798 | use_existing_requirements, 799 | extra_pip_install_args, 800 | path_to_get_pip_and_python_embedded_zip, 801 | icon_file, 802 | verbose, 803 | ): 804 | """ 805 | Package your python script(s) as a stand-alone Windows application. 806 | 807 | Basic usage: 808 | 809 | $ pyvan main.py 810 | 811 | This command will try to make main.py the entrypoint of your application. 812 | It will automatically try to resolve the required requirements by running `pipreqs` in your `input_dir`. 813 | Next, it will attempt to search and install an embedded python distribution using the generated requirements. 814 | Finally, it will link the packaged sources to the packaged python distribution using a batch file. 815 | The stand-alone application can then be found inside the generated `build_dir` ("dist") folder. 816 | 817 | """ 818 | build( 819 | main_file_name=main_file_name, 820 | show_console=show_console, 821 | input_dir=input_dir, 822 | build_dir=build_dir, 823 | pydist_sub_dir=pydist_sub_dir, 824 | source_sub_dir=source_sub_dir, 825 | use_pipreqs=use_pipreqs, 826 | python_version=python_version, 827 | include_modules=include_modules, 828 | exclude_modules=exclude_modules, 829 | install_only_these_modules=install_only_these_modules, 830 | use_existing_requirements=use_existing_requirements, 831 | extra_pip_install_args=extra_pip_install_args, 832 | icon_file=icon_file, 833 | path_to_get_pip_and_python_embedded_zip="" 834 | if path_to_get_pip_and_python_embedded_zip is None 835 | else path_to_get_pip_and_python_embedded_zip, 836 | verbose=verbose, 837 | ) 838 | 839 | 840 | if __name__ == "__main__": 841 | cli() 842 | --------------------------------------------------------------------------------