Browse Source

Use docker to deploy

pull/1/head
Wesley Kerfoot 5 years ago
parent
commit
c81f993985
  1. 21
      app/Dockerfile
  2. 0
      app/admin.py
  3. 0
      app/images/88x31.png
  4. 0
      app/list.js
  5. 1590
      app/package-lock.json
  6. 4
      app/package.json
  7. 5
      app/posts.py
  8. 0
      app/projects.py
  9. 36
      app/requirements.txt
  10. 10
      app/rollup.config.editor.js
  11. 25
      app/rollup.config.riotblog.js
  12. 0
      app/scripts/about.tag
  13. 2
      app/scripts/app.tag
  14. 0
      app/scripts/blog.tag
  15. 0
      app/scripts/browse.tag
  16. 0
      app/scripts/categories.tag
  17. 0
      app/scripts/categoryfilter.tag
  18. 0
      app/scripts/categorymodal.tag
  19. 0
      app/scripts/column.tag
  20. 0
      app/scripts/editor.js
  21. 0
      app/scripts/editor.tag
  22. 0
      app/scripts/githubsocial.tag
  23. 0
      app/scripts/grid.js
  24. 0
      app/scripts/grid.tag
  25. 0
      app/scripts/headersocial.tag
  26. 0
      app/scripts/lenses.js
  27. 0
      app/scripts/links.tag
  28. 0
      app/scripts/loading.tag
  29. 0
      app/scripts/menu.tag
  30. 0
      app/scripts/navtab.tag
  31. 0
      app/scripts/post-content.tag
  32. 0
      app/scripts/post.tag
  33. 0
      app/scripts/postcontrols.tag
  34. 0
      app/scripts/posts.tag
  35. 0
      app/scripts/postsview.tag
  36. 4
      app/scripts/projects.tag
  37. 0
      app/scripts/raw.tag
  38. 0
      app/scripts/riotblog.js
  39. 0
      app/scripts/row.tag
  40. 0
      app/scripts/sidebar.tag
  41. 0
      app/scripts/social.tag
  42. 0
      app/scripts/zipper.js
  43. 0
      app/styles/animate.min.css
  44. 0
      app/styles/projects.scss
  45. 0
      app/styles/riotblog.scss
  46. 0
      app/styles/spectre.min.css
  47. 4
      app/templates/index.html
  48. 0
      app/templates/login.html
  49. 0
      app/templates/projects.html
  50. 0
      app/templates/write.html
  51. 56
      app/website.py
  52. 1199
      app/yarn.lock
  53. 13
      blog.service
  54. 13
      blog_test.service
  55. 26
      couchdb/blogPosts.json
  56. 15
      docker-compose.yml
  57. 98
      fabfile.py
  58. 49
      requirements.txt
  59. 5
      watch.sh

21
app/Dockerfile

@ -0,0 +1,21 @@
FROM ubuntu:latest
MAINTAINER wes kerfoot "wjak56@gmail.com"
RUN apt-get update -y
RUN apt-get install -y python3-pip python3-dev build-essential
RUN apt-get install -y curl
RUN curl -sL https://deb.nodesource.com/setup_11.x | bash
RUN apt-get install -y sassc nodejs
RUN npm install -g uglifycss uglifyjs
COPY . ./src
WORKDIR /src
RUN npm install
RUN pip3 install -r /src/requirements.txt
RUN mkdir -p ./build/styles ./build/scripts
RUN npm run-script build_riotblog
RUN npm run-script build_editor
RUN sassc ./styles/riotblog.scss > ./styles/riotblog.intermediate.min.css
RUN uglifycss ./styles/*.css > ./build/styles/riotblog.min.css
ENTRYPOINT ["python3"]
ENV RIOTBLOG_SETTINGS "/src/riotblog_local.cfg"
CMD ["/src/website.py"]
EXPOSE 80

0
src/admin.py → app/admin.py

0
src/images/88x31.png → app/images/88x31.png

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

0
list.js → app/list.js

1590
app/package-lock.json

File diff suppressed because it is too large

4
package.json → app/package.json

@ -4,8 +4,8 @@
"description": "Riot Blog",
"main": "src/scripts/riotblog.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"build": "rollup src/scripts/riotblog.js"
"build_riotblog": "rollup --input /src/scripts/riotblog.js -c /src/rollup.config.riotblog.js",
"build_editor": "rollup --input /src/scripts/editor.js -c /src/rollup.config.editor.js"
},
"repository": {
"type": "git",

5
src/posts.py → app/posts.py

@ -27,11 +27,14 @@ class Posts:
if port is None:
port = "5984"
host = "127.0.0.1"
port = "5984"
self.client = couchdb.Server("http://%s:%s" % (host, port))
self.client.credentials = (user, password)
self.db = self.client[name]
self.db = self.client["blog"]
self.iterpost = self.postIterator("blogPosts/blog-posts")

0
src/projects.py → app/projects.py

36
app/requirements.txt

@ -0,0 +1,36 @@
appconfig==0.1
appdirs==1.4.3
asn1crypto==0.24.0
certifi==2018.11.29
cffi==1.11.5
chardet==3.0.4
Click==7.0
CouchDB==1.2
cryptography==2.4.2
dominate==2.3.5
Flask==0.12.4
flask-appconfig==0.11.1
Flask-Bootstrap==3.3.7.1
Flask-Cache==0.13.1
Flask-Login==0.4.1
flask-marshmallow==0.9.0
Flask-WTF==0.14.2
greenlet==0.4.15
idna==2.8
itsdangerous==1.1.0
Jinja2==2.10
lxml==4.2.5
MarkupSafe==1.1.0
marshmallow==2.17.0
mistune==0.8.4
packaging==18.0
pycparser==2.19
pyparsing==2.3.0
python-memcached==1.59
requests==2.21.0
six==1.12.0
urllib3==1.24.1
uWSGI==2.0.17.1
visitor==0.1.3
Werkzeug==0.14.1
WTForms==2.2.1

10
rollup.config.js → app/rollup.config.editor.js

@ -5,6 +5,7 @@ import buble from 'rollup-plugin-buble'
import uglify from 'rollup-plugin-uglify';
function makeBundle(item) {
console.log(item);
var entry = item[0];
var dest = item[1];
return {
@ -21,11 +22,4 @@ function makeBundle(item) {
};
}
const items = [
["src/scripts/riotblog.js", "build/bundle.js"],
["src/scripts/editor.js", "build/editor.bundle.js"]
];
var bundles = items.map(makeBundle);
export default bundles;
export default makeBundle(["./scripts/editor.js", "./build/scripts/editor.bundle.js"]);

25
app/rollup.config.riotblog.js

@ -0,0 +1,25 @@
import riot from 'rollup-plugin-riot'
import nodeResolve from 'rollup-plugin-node-resolve'
import commonjs from 'rollup-plugin-commonjs'
import buble from 'rollup-plugin-buble'
import uglify from 'rollup-plugin-uglify';
function makeBundle(item) {
console.log(item);
var entry = item[0];
var dest = item[1];
return {
entry: entry,
dest: dest,
plugins: [
riot(),
nodeResolve({ jsnext: true, preferBuiltins: false}),
commonjs(),
buble(),
uglify()
],
format: 'iife'
};
}
export default makeBundle(["./scripts/riotblog.js", "./build/scripts/riotblog.min.js"]);

0
src/scripts/about.tag → app/scripts/about.tag

2
src/scripts/app.tag → app/scripts/app.tag

@ -23,7 +23,7 @@
}
>
<githubsocial
link="https://github.com/nisstyre56"
link="https://github.com/weskerfoot"
>
</githubsocial>
</div>

0
src/scripts/blog.tag → app/scripts/blog.tag

0
src/scripts/browse.tag → app/scripts/browse.tag

0
src/scripts/categories.tag → app/scripts/categories.tag

0
src/scripts/categoryfilter.tag → app/scripts/categoryfilter.tag

0
src/scripts/categorymodal.tag → app/scripts/categorymodal.tag

0
src/scripts/column.tag → app/scripts/column.tag

0
src/scripts/editor.js → app/scripts/editor.js

0
src/scripts/editor.tag → app/scripts/editor.tag

0
src/scripts/githubsocial.tag → app/scripts/githubsocial.tag

0
src/scripts/grid.js → app/scripts/grid.js

0
src/scripts/grid.tag → app/scripts/grid.tag

0
src/scripts/headersocial.tag → app/scripts/headersocial.tag

0
src/scripts/lenses.js → app/scripts/lenses.js

0
src/scripts/links.tag → app/scripts/links.tag

0
src/scripts/loading.tag → app/scripts/loading.tag

0
src/scripts/menu.tag → app/scripts/menu.tag

0
src/scripts/navtab.tag → app/scripts/navtab.tag

0
src/scripts/post-content.tag → app/scripts/post-content.tag

0
src/scripts/post.tag → app/scripts/post.tag

0
src/scripts/postcontrols.tag → app/scripts/postcontrols.tag

0
src/scripts/posts.tag → app/scripts/posts.tag

0
src/scripts/postsview.tag → app/scripts/postsview.tag

4
src/scripts/projects.tag → app/scripts/projects.tag

@ -75,7 +75,7 @@
"margin-bottom" : "8px"
}
}
src={"https://ghbtns.com/github-btn.html?user=nisstyre56&repo="+this.project().name+"&type=star&count=false&size=large"}
src={"https://ghbtns.com/github-btn.html?user=weskerfoot&repo="+this.project().name+"&type=star&count=false&size=large"}
frameborder="0"
scrolling="0"
width="72px"
@ -90,7 +90,7 @@
"float" : "right"
}
}
src={"https://ghbtns.com/github-btn.html?user=nisstyre56&repo="+this.project().name+"&type=fork&count=false&size=large"}
src={"https://ghbtns.com/github-btn.html?user=weskerfoot&repo="+this.project().name+"&type=fork&count=false&size=large"}
frameborder="0"
scrolling="0"
width="72px"

0
src/scripts/raw.tag → app/scripts/raw.tag

0
src/scripts/riotblog.js → app/scripts/riotblog.js

0
src/scripts/row.tag → app/scripts/row.tag

0
src/scripts/sidebar.tag → app/scripts/sidebar.tag

0
src/scripts/social.tag → app/scripts/social.tag

0
src/scripts/zipper.js → app/scripts/zipper.js

0
src/styles/animate.min.css → app/styles/animate.min.css

0
src/styles/projects.scss → app/styles/projects.scss

0
src/styles/riotblog.scss → app/styles/riotblog.scss

0
src/styles/spectre.min.css → app/styles/spectre.min.css

4
src/templates/index.html → app/templates/index.html

@ -15,10 +15,10 @@
<noscript>{{ postcontent['content']|safe }}</noscript>
<div data-is="app"></div>
<footer>
<script async type="text/javascript" src="/scripts/primop.min.js"></script>
<script async type="text/javascript" src="/scripts/riotblog.min.js"></script>
<script type="text/javascript">
(function(w){"use strict";var loadCSS=function(href,before,media){var doc=w.document;var ss=doc.createElement("link");var ref;if(before){ref=before}else{var refs=(doc.body||doc.getElementsByTagName("head")[0]).childNodes;ref=refs[refs.length-1]}var sheets=doc.styleSheets;ss.rel="stylesheet";ss.href=href;ss.media="only x";function ready(cb){if(doc.body){return cb()}setTimeout(function(){ready(cb)})}ready(function(){ref.parentNode.insertBefore(ss,before?ref:ref.nextSibling)});var onloadcssdefined=function(cb){var resolvedHref=ss.href;var i=sheets.length;while(i--){if(sheets[i].href===resolvedHref){return cb()}}setTimeout(function(){onloadcssdefined(cb)})};function loadCB(){if(ss.addEventListener){ss.removeEventListener("load",loadCB)}ss.media=media||"all"}if(ss.addEventListener){ss.addEventListener("load",loadCB)}ss.onloadcssdefined=onloadcssdefined;onloadcssdefined(loadCB);return ss};if(typeof exports!=="undefined"){exports.loadCSS=loadCSS}else{w.loadCSS=loadCSS}})(typeof global!=="undefined"?global:this);(function(w){if(!w.loadCSS){return}var rp=loadCSS.relpreload={};rp.support=function(){try{return w.document.createElement("link").relList.supports("preload")}catch(e){return false}};rp.poly=function(){var links=w.document.getElementsByTagName("link");for(var i=0;i<links.length;i++){var link=links[i];if(link.rel==="preload"&&link.getAttribute("as")==="style"){w.loadCSS(link.href,link,link.getAttribute("media"));link.rel=null}}};if(!rp.support()){rp.poly();var run=w.setInterval(rp.poly,300);if(w.addEventListener){w.addEventListener("load",function(){rp.poly();w.clearInterval(run)})}if(w.attachEvent){w.attachEvent("onload",function(){w.clearInterval(run)})}}})(this);
var hrefs = ['/styles/primop.min.css', 'https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css'];
var hrefs = ['/styles/riotblog.min.css', 'https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css'];
for (var i = 0, l = hrefs.length; i < l; i++) {
loadCSS(hrefs[i]);
}

0
src/templates/login.html → app/templates/login.html

0
src/templates/projects.html → app/templates/projects.html

0
src/templates/write.html → app/templates/write.html

56
src/website.py → app/website.py

@ -13,16 +13,16 @@ from flask.ext.cache import Cache
from urllib.parse import quote, unquote
from json import dumps, loads
from admin import Admin
from werkzeug.contrib.cache import MemcachedCache
#from werkzeug.contrib.cache import simpleCache
from posts import Posts
from projects import getProjects
memcache = MemcachedCache(['127.0.0.1:11211'])
cache = Cache(config={'CACHE_TYPE': 'memcached'})
login_manager = LoginManager()
#memcache = simpleCache(['127.0.0.1:11211'])
cache = Cache(config={'CACHE_TYPE': 'simple'})
page_titles = {
"about" : "About Me",
"projects" : "Software",
@ -32,13 +32,13 @@ page_titles = {
def cacheit(key, thunk):
"""
Explicit memcached caching
Explicit simple caching
"""
cached = memcache.get(quote(key))
cached = cache.get(quote(key))
if cached is None:
print("cache miss for %s" % key)
result = thunk()
memcache.set(quote(key), result)
cache.set(quote(key), result)
return result
print("cache hit for %s" % key)
return cached
@ -60,7 +60,7 @@ def get_initial():
def NeverWhere(configfile=None):
app = Flask(__name__)
app.config["TEMPLATES_AUTO_RELOAD"] = True
app.config["COUCHDB_SERVER"] = "http://localhost:5984"
app.config["COUCHDB_SERVER"] = "http://127.0.0.1:5984"
app.config["COUCHDB_DATABASE"] = "blog"
#def favicon():
#return send_from_directory("/srv/http/goal/favicon.ico",
@ -69,6 +69,14 @@ def NeverWhere(configfile=None):
print(environ["RIOTBLOG_SETTINGS"])
app.config.from_envvar('RIOTBLOG_SETTINGS')
@app.route("/styles/<path:path>")
def send_styles(path):
return send_from_directory("build/styles", path)
@app.route("/scripts/<path:path>")
def send_scripts(path):
return send_from_directory("build/scripts", path)
# Set template variables to be injected
@app.context_processor
def inject_variables():
@ -246,7 +254,7 @@ def NeverWhere(configfile=None):
"draft" : draft
}
memcache.clear()
cache.clear()
return posts.savepost(**post)
@app.route("/blog/glinks/", methods=("GET",))
@ -288,24 +296,26 @@ def NeverWhere(configfile=None):
return app
app = NeverWhere()
if __name__ == "__main__":
@app.teardown_appcontext
def teardown_couchdb(exception):
posts = getattr(g, 'posts', None)
if posts is not None:
del posts.db
app = NeverWhere()
posts = LocalProxy(get_posts)
initial_post = LocalProxy(get_initial)
@app.teardown_appcontext
def teardown_couchdb(exception):
posts = getattr(g, 'posts', None)
if posts is not None:
del posts.db
login_manager.init_app(app)
csrf = CSRFProtect()
csrf = CSRFProtect()
csrf.init_app(app)
csrf.init_app(app)
cache.init_app(app)
cache.init_app(app)
posts = LocalProxy(get_posts)
if __name__ == "__main__":
NeverWhere().run(host="localhost", port=8001, debug=True)
initial_post = LocalProxy(get_initial)
login_manager.init_app(app)
app.run(host="0.0.0.0", port=80, debug=True)

1199
yarn.lock → app/yarn.lock

File diff suppressed because it is too large

13
blog.service

@ -1,13 +0,0 @@
[Unit]
Description=My Blargh
After=network.target
[Service]
User=http
Group=http
WorkingDirectory=/srv/http/riotblog
ExecStart=/usr/bin/uwsgi --single-interpreter --ini /srv/http/riotblog/blog.ini
Environment="RIOTBLOG_SETTINGS=/srv/http/riotblog/riotblog_prod.cfg"
[Install]
WantedBy=multi.user.target

13
blog_test.service

@ -1,13 +0,0 @@
[Unit]
Description=My Blargh
After=network.target
[Service]
User=http
Group=http
WorkingDirectory=/srv/http/riotblog
ExecStart=/usr/bin/uwsgi --single-interpreter --ini /srv/http/riotblog/blog.ini
Environment="RIOTBLOG_SETTINGS=/srv/http/riotblog/riotblog_local.cfg"
[Install]
WantedBy=multi.user.target

26
couchdb/blogPosts.json

@ -0,0 +1,26 @@
{
"_id": "_design/blogPosts",
"_rev": "128-d6bf6a41f78d140bdbb71a8bed3c2554",
"views": {
"blog-posts": {
"map": "function (doc) {\n if (doc.type == \"post\" && !doc.draft) {\n emit(doc._id.slice(-8), 1);\n }\n}\n\n\n\n\n"
},
"links": {
"map": "function (doc) {\n if (doc.type == \"links\") { emit(doc._id, 1); }\n}"
},
"categories": {
"map": "function (doc) {\n if (doc.categories !== undefined && !doc.draft) {\n emit([\"categories\", doc.categories], 1);\n }\n}",
"reduce": "function (keys, values, rereduce) {\n return null;\n}"
},
"format": {
"map": "function (doc) {\n if (doc.categories !== undefined && !doc.draft) {\n emit(doc._id.slice(-8),\n {\n \"categories\" : doc.categories,\n \"author\" : doc.author,\n \"title\" : doc.title,\n \"id\" : doc._id.slice(-8),\n \"content\" : doc.content.slice(0, 250)\n });\n }\n}"
},
"unpublished": {
"map": "function (doc) {\n if (doc.type == \"post\") {\n emit(doc._id.slice(-8), 1);\n }\n}"
}
},
"lists": {
"categories": "function(head, req) { var row, results; var results = []; var categories = (req.query.categories !== undefined ? JSON.parse(req.query.categories) : []); var num = parseInt(req.query.num, 10); var getlast = req.query.getlast; var full_results; if (getlast == undefined) { while (results.length < num && (row = getRow())) { if (categories.length == 0 || categories.some(function(c) { return row.value.categories.indexOf(c) !== -1; })) { results.push([row.value.categories, row.value]); } } full_results = results; } else { while (row = getRow()) { if (categories.length == 0 || categories.some(function(c) { return row.value.categories.indexOf(c) !== -1; })) { results.push([row.value.categories, row.value]); } } full_results = results.slice(results.length-(num+1), results.length-1); } return JSON.stringify({q : req.query.categories, results : full_results});}"
},
"language": "javascript"
}

15
docker-compose.yml

@ -0,0 +1,15 @@
version: '3'
services:
web:
build: app
ports:
- "80"
environment:
SECRET_KEY: "asdsdgfdgsdfdsf"
ADMIN_PASSWORD: "***REMOVED***"
JSONIFY_PRETTYPRINT_REGULAR: "False"
network_mode: host
couch:
image: couchdb
ports:
- "127.0.0.1:5984:5984"

98
fabfile.py

@ -1,98 +0,0 @@
from __future__ import with_statement
from fabric.api import *
from fabric.contrib.console import confirm
from fabric.contrib.project import rsync_project
import fabric.operations as op
env.hosts = ["wes@mgoal.ca:444"]
@task
def buildScss():
with lcd("./build"):
local("sassc ../src/styles/riotblog.scss > styles/riotblog.min.css")
local("sassc ../src/styles/projects.scss > styles/projects.min.css")
@task
def buildJS():
local("rollup -c rollup.config.js")
local("uglifyjs build/bundle.js -c > build/scripts/primop.min.js")
local("uglifyjs build/editor.bundle.js -c > build/scripts/editor.min.js")
@task
def buildVenv():
with cd("~/build"):
run("virtualenv -p $(which python3) ~/build/venv")
with prefix("source ~/build/venv/bin/activate"):
run("pip3 install -r requirements.txt")
@task
def buildLocalVenv():
with lcd("~/primop.me/build"):
local("virtualenv -p $(which python3) ~/primop.me/build/venv")
with prefix("source ~/primop.me/build/venv/bin/activate"):
local("pip3 install -r requirements.txt")
@task
def copyFiles():
local("cp ./{blog.ini,blog.service,requirements.txt} ./build/")
local("cp ./src/*py ./build/")
local("cp *.cfg ./build/")
local("cp ./src/styles/*.css ./build/styles/")
local("cp ./src/images/*png ./build/images/")
local("uglifycss ./build/styles/*css > ./build/styles/primop.min.css")
local("cp -r ./src/templates ./build/templates")
@task
def upload():
run("mkdir -p ~/build")
rsync_project(local_dir="./build/", remote_dir="~/build/", delete=False, exclude=[".git"])
@task
def serveUp():
sudo("rm -rf /srv/http/riotblog_static")
sudo("rm -fr /srv/http/riotblog")
sudo("mkdir -p /srv/http/riotblog_static")
sudo("cp -r /home/wes/build/ /srv/http/riotblog/")
sudo("cp -r /home/wes/build/{styles,scripts,images} /srv/http/riotblog_static")
sudo("cp /home/wes/build/blog.service /etc/systemd/system/blog.service")
sudo("systemctl daemon-reload")
sudo("systemctl enable blog.service")
sudo("systemctl restart blog.service")
sudo("systemctl restart memcached")
@task(default=True)
def build():
local("rm -rf ./build")
local("mkdir -p build/{scripts,styles,images}")
buildScss()
buildJS()
copyFiles()
upload()
buildVenv()
serveUp()
@task
def update():
local("mkdir -p build/{scripts,styles,images}")
buildScss()
buildJS()
copyFiles()
upload()
serveUp()
@task
def locbuild():
local("rm -rf ./build")
local("mkdir -p build/{scripts,styles,images}")
local("cp requirements.txt ./build/requirements.txt")
buildLocalVenv()
buildScss()
buildJS()
copyFiles()
local("sudo rm -fr /srv/http/riotblog")
local("sudo mkdir -p /srv/http/riotblog")
local("sudo cp -r ./build/* /srv/http/riotblog/")
local("sudo cp /home/wes/primop.me/blog_test.service /etc/systemd/system/blog.service")
local("sudo systemctl daemon-reload")
local("sudo systemctl enable blog.service")
local("sudo systemctl restart blog.service")

49
requirements.txt

@ -1,49 +0,0 @@
appdirs==1.4.3
certifi==2017.7.27.1
cffi==1.9.1
chardet==3.0.4
click==6.7
CouchDB==1.1
cryptography==1.7.2
dominate==2.3.1
elasticsearch==5.1.0
elasticsearch-dsl==5.1.0
Fabric==1.13.1
Flask==0.12
flask-appconfig==0.11.1
Flask-Bootstrap==3.3.7.1
Flask-Cache==0.13.1
Flask-Login==0.4.0
flask-marshmallow==0.7.0
Flask-WTF==0.14.2
greenlet==0.4.12
idna==2.5
itsdangerous==0.24
Jinja2==2.9.4
lxml==3.7.2
MarkupSafe==0.23
marshmallow==2.13.5
mistune==0.7.4
packaging==16.8
paramiko==2.1.1
pbr==1.10.0
pyasn1==0.2.1
pycparser==2.17
pyparsing==2.2.0
PySocks==1.6.6
python-dateutil==2.6.0
python-memcached==1.58
pyxdg==0.25
requests==2.18.3
simplejson==3.11.1
six==1.10.0
stevedore==1.20.0
urllib3==1.22
uWSGI==2.0.14
virtualenv==15.1.0
virtualenv-clone==0.2.6
virtualenvwrapper==4.7.2
visitor==0.1.3
Werkzeug==0.11.15
WTForms==2.1
youtube-dl==2017.1.28

5
watch.sh

@ -1,5 +0,0 @@
#! /usr/bin/bash
while inotifywait -qqre modify "./src"; do
fab locbuild
done
Loading…
Cancel
Save