Unverified Commit b41667b8 authored by Marcel Stör's avatar Marcel Stör Committed by GitHub
Browse files

Master drop #2486

Dev -> Master Release 10
parents f99f295d 0abb2617
-- ***************************************************************************
-- Graphics Test
--
-- This script executes several features of u8glib to test their Lua bindings.
--
-- Note: It is prepared for SSD1306-based displays. Select your connectivity
-- type by calling either init_i2c_display() or init_spi_display() at
-- the bottom of this file.
--
-- ***************************************************************************
-- setup I2c and connect display
function init_i2c_display()
-- SDA and SCL can be assigned freely to available GPIOs
local sda = 5 -- GPIO14
local scl = 6 -- GPIO12
local sla = 0x3c
i2c.setup(0, sda, scl, i2c.SLOW)
disp = u8g.ssd1306_128x64_i2c(sla)
end
-- setup SPI and connect display
function init_spi_display()
-- Hardware SPI CLK = GPIO14
-- Hardware SPI MOSI = GPIO13
-- Hardware SPI MISO = GPIO12 (not used)
-- Hardware SPI /CS = GPIO15 (not used)
-- CS, D/C, and RES can be assigned freely to available GPIOs
local cs = 8 -- GPIO15, pull-down 10k to GND
local dc = 4 -- GPIO2
local res = 0 -- GPIO16
spi.setup(1, spi.MASTER, spi.CPOL_LOW, spi.CPHA_LOW, 8, 8)
-- we won't be using the HSPI /CS line, so disable it again
gpio.mode(8, gpio.INPUT, gpio.PULLUP)
disp = u8g.ssd1306_128x64_hw_spi(cs, dc, res)
end
-- graphic test components
function prepare()
disp:setFont(u8g.font_6x10)
disp:setFontRefHeightExtendedText()
disp:setDefaultForegroundColor()
disp:setFontPosTop()
end
function box_frame(a)
disp:drawStr(0, 0, "drawBox")
disp:drawBox(5, 10, 20, 10)
disp:drawBox(10+a, 15, 30, 7)
disp:drawStr(0, 30, "drawFrame")
disp:drawFrame(5, 10+30, 20, 10)
disp:drawFrame(10+a, 15+30, 30, 7)
end
function disc_circle(a)
disp:drawStr(0, 0, "drawDisc")
disp:drawDisc(10, 18, 9)
disp:drawDisc(24+a, 16, 7)
disp:drawStr(0, 30, "drawCircle")
disp:drawCircle(10, 18+30, 9)
disp:drawCircle(24+a, 16+30, 7)
end
function r_frame(a)
disp:drawStr(0, 0, "drawRFrame/Box")
disp:drawRFrame(5, 10, 40, 30, a+1)
disp:drawRBox(50, 10, 25, 40, a+1)
end
function stringtest(a)
disp:drawStr(30+a, 31, " 0")
disp:drawStr90(30, 31+a, " 90")
disp:drawStr180(30-a, 31, " 180")
disp:drawStr270(30, 31-a, " 270")
end
function line(a)
disp:drawStr(0, 0, "drawLine")
disp:drawLine(7+a, 10, 40, 55)
disp:drawLine(7+a*2, 10, 60, 55)
disp:drawLine(7+a*3, 10, 80, 55)
disp:drawLine(7+a*4, 10, 100, 55)
end
function triangle(a)
local offset = a
disp:drawStr(0, 0, "drawTriangle")
disp:drawTriangle(14,7, 45,30, 10,40)
disp:drawTriangle(14+offset,7-offset, 45+offset,30-offset, 57+offset,10-offset)
disp:drawTriangle(57+offset*2,10, 45+offset*2,30, 86+offset*2,53)
disp:drawTriangle(10+offset,40+offset, 45+offset,30+offset, 86+offset,53+offset)
end
function ascii_1()
local x, y, s
disp:drawStr(0, 0, "ASCII page 1")
for y = 0, 5, 1 do
for x = 0, 15, 1 do
s = y*16 + x + 32
disp:drawStr(x*7, y*10+10, string.char(s))
end
end
end
function extra_page(a)
disp:drawStr(0, 12, "setScale2x2")
disp:setScale2x2()
disp:drawStr(0, 6+a, "setScale2x2")
disp:undoScale()
end
-- the draw() routine
function draw(draw_state)
local component = bit.rshift(draw_state, 3)
prepare()
if (component == 0) then
box_frame(bit.band(draw_state, 7))
elseif (component == 1) then
disc_circle(bit.band(draw_state, 7))
elseif (component == 2) then
r_frame(bit.band(draw_state, 7))
elseif (component == 3) then
stringtest(bit.band(draw_state, 7))
elseif (component == 4) then
line(bit.band(draw_state, 7))
elseif (component == 5) then
triangle(bit.band(draw_state, 7))
elseif (component == 6) then
ascii_1()
elseif (component == 7) then
extra_page(bit.band(draw_state, 7))
end
end
function draw_loop()
-- Draws one page and schedules the next page, if there is one
local function draw_pages()
draw(draw_state)
if disp:nextPage() then
node.task.post(draw_pages)
else
node.task.post(graphics_test)
end
end
-- Restart the draw loop and start drawing pages
disp:firstPage()
node.task.post(draw_pages)
end
function graphics_test()
if (draw_state <= 7 + 8*8) then
draw_state = draw_state + 1
else
print("--- Restarting Graphics Test ---")
draw_state = 0
end
print("Heap: " .. node.heap())
-- retrigger draw_loop
node.task.post(draw_loop)
end
draw_state = 0
init_i2c_display()
--init_spi_display()
print("--- Starting Graphics Test ---")
node.task.post(draw_loop)
-- ***************************************************************************
-- Rotation Test
--
-- This script executes the rotation features of u8glib to test their Lua
-- integration.
--
-- Note: It is prepared for SSD1306-based displays. Select your connectivity
-- type by calling either init_i2c_display() or init_spi_display() at
-- the bottom of this file.
--
-- ***************************************************************************
-- setup I2c and connect display
function init_i2c_display()
-- SDA and SCL can be assigned freely to available GPIOs
local sda = 5 -- GPIO14
local scl = 6 -- GPIO12
local sla = 0x3c
i2c.setup(0, sda, scl, i2c.SLOW)
disp = u8g.ssd1306_128x64_i2c(sla)
end
-- setup SPI and connect display
function init_spi_display()
-- Hardware SPI CLK = GPIO14
-- Hardware SPI MOSI = GPIO13
-- Hardware SPI MISO = GPIO12 (not used)
-- Hardware SPI /CS = GPIO15 (not used)
-- CS, D/C, and RES can be assigned freely to available GPIOs
local cs = 8 -- GPIO15, pull-down 10k to GND
local dc = 4 -- GPIO2
local res = 0 -- GPIO16
spi.setup(1, spi.MASTER, spi.CPOL_LOW, spi.CPHA_LOW, 8, 8)
-- we won't be using the HSPI /CS line, so disable it again
gpio.mode(8, gpio.INPUT, gpio.PULLUP)
disp = u8g.ssd1306_128x64_hw_spi(cs, dc, res)
end
-- the draw() routine
function draw()
disp:setFont(u8g.font_6x10)
disp:drawStr( 0+0, 20+0, "Hello!")
disp:drawStr( 0+2, 20+16, "Hello!")
disp:drawBox(0, 0, 3, 3)
disp:drawBox(disp:getWidth()-6, 0, 6, 6)
disp:drawBox(disp:getWidth()-9, disp:getHeight()-9, 9, 9)
disp:drawBox(0, disp:getHeight()-12, 12, 12)
end
function rotate()
if (next_rotation < tmr.now() / 1000) then
if (dir == 0) then
disp:undoRotation()
elseif (dir == 1) then
disp:setRot90()
elseif (dir == 2) then
disp:setRot180()
elseif (dir == 3) then
disp:setRot270()
end
dir = dir + 1
dir = bit.band(dir, 3)
-- schedule next rotation step in 1000ms
next_rotation = tmr.now() / 1000 + 1000
end
end
function rotation_test()
print("--- Starting Rotation Test ---")
dir = 0
next_rotation = 0
local loopcnt
for loopcnt = 1, 100, 1 do
rotate()
disp:firstPage()
repeat
draw(draw_state)
until disp:nextPage() == false
tmr.delay(100000)
tmr.wdclr()
end
print("--- Rotation Test done ---")
end
--init_i2c_display()
init_spi_display()
rotation_test()
# FTPServer Module
This Lua module implementation provides a basic FTP server for the ESP8266.
It has been tested against a number of Table, Windows and Linux FTP clients
and browsers.
It provides a limited subset of FTP commands that enable such clients to
tranfer files to and from the ESP's file system. Only one server can be
started at any one time, but this server can support multiple connected
sessions (some FTP clients use multiple sessions and so require this
feature).
### Limitations
- FTP over SSH or TLS is not currently supported so transfer is unencrypted.
- The client session , must, authentical against a single user/password.
- Only the SPIFFS filesystem is currently supported, so changing directories is treated as a NO-OP.
- This implementation has been optimised for running in LFS.
- Only PASV mode is supported as the `net` module does not allow static allocation of outbound sockets.
### Notes
The coding style adopted here is more similar to best practice for normal (PC)
module implementations, as using LFS permits a bias towards clarity of coding
over brevity. It includes extra logic to handle some of the edge case issues more
robustly. It also uses a standard forward reference coding pattern to allow the
code to be laid out in main routine, subroutine order.
Most FTP clients are capable of higher transfer rates than the ESP SPIFFS write
throughput, so the server uses TCP flow control to limit upload rates to the
ESP.
aThe following FTP commands are supported:
- with no parameter: CDUP, NOOP, PASV, PWD, QUIT, SYST
- with one parameter: CWD, DELE, MODE, PASS, PORT, RNFR, RNTO, SIZE, TYPE, USER
- xfer commands: LIST, NLST, RETR, STOR
This implementation is by Terry Ellison, but I wish to acknowledge the inspration
and hard work by [Neronix](https://github.com/NeiroNx) that made this possible.
## createServer()
Create the FTP server on the standard ports 20 and 21. The global variable `FTP`
is set to the server object.
#### Syntax
`FTP.createServer(user, pass[, dbgFlag])`
#### Parameters
- `user` - Username for access to the server
- `pass` - Password for access to the server
- `dbgFlag` - optional flag. If set true then internal debug output is printed
#### Returns
- N/A
#### Example
```Lua
require("ftpserver").createServer('user', 'password')
```
## open()
Wrapper to createServer() which also connects to the WiFi channel.
#### Syntax
`FTP.open(user, pass, ssid, wifipwd, dbgFlag)`
#### Parameters
- `user` - Username for access to the server
- `pass` - Password for access to the server
- `ssid` - SSID for Wifi service
- `wifipwd` - password for Wifi service
- `dbgFlag` - optional flag. If set true then internal debug output is printed
#### Returns
- N/A
#### Example
```Lua
require("ftpserver").open('myWifi', 'wifiPassword', 'user', 'password')
```
## close()
Close down server including any sockets and return all resouces to Lua. Note that
this include removing the FTP global variable and package references.
#### Syntax
`FTP.close()`
#### Parameters
- none
#### Returns
- nil
#### Example
```Lua
FTP.close()
```
--[[ A simple ftp server
This is my implementation of a FTP server using Github user Neronix's
example as inspriration, but as a cleaner Lua implementation that has been
optimised for use in LFS. The coding style adopted here is more similar to
best practice for normal (PC) module implementations, as using LFS enables
me to bias towards clarity of coding over brevity. It includes extra logic
to handle some of the edge case issues more robustly. It also uses a
standard forward reference coding pattern to allow the code to be laid out
in main routine, subroutine order.
The app will only call one FTP.open() or FTP.createServer() at any time,
with any multiple calls requected, so FTP is a singleton static object.
However there is nothing to stop multiple clients connecting to the FTP
listener at the same time, and indeed some FTP clients do use multiple
connections, so this server can accept and create multiple CON objects.
Each CON object can also have a single DATA connection.
Note that FTP also exposes a number of really private properties (which
could be stores in local / upvals) as FTP properties for debug purposes.
]]
local file,net,wifi,node,string,table,tmr,pairs,print,pcall, tostring =
file,net,wifi,node,string,table,tmr,pairs,print,pcall, tostring
local post = node.task.post
local FTP, cnt = {client = {}}, 0
-- Local functions
local processCommand -- function(cxt, sock, data)
local processBareCmds -- function(cxt, cmd)
local processSimpleCmds -- function(cxt, cmd, arg)
local processDataCmds -- function(cxt, cmd, arg)
local dataServer -- function(cxt, n)
local ftpDataOpen -- function(dataSocket)
-- Note these routines all used hoisted locals such as table and debug as
-- upvals for performance (ROTable lookup is slow on NodeMCU Lua), but
-- data upvals (e.g. FTP) are explicitly list is -- "upval:" comments.
-- Note that the space between debug and the arglist is there for a reason
-- so that a simple global edit " debug(" -> "-- debug(" or v.v. to
-- toggle debug compiled into the module.
local function debug (fmt, ...) -- upval: cnt (, print, node, tmr)
if not FTP.debug then return end
if (...) then fmt = fmt:format(...) end
print(node.heap(),fmt)
cnt = cnt + 1
if cnt % 10 then tmr.wdclr() end
end
--------------------------- Set up the FTP object ----------------------------
-- FTP has three static methods: open, createServer and close
------------------------------------------------------------------------------
-- optional wrapper around createServer() which also starts the wifi session
function FTP.open(user, pass, ssid, pwd, dbgFlag) -- upval: FTP (, wifi, tmr, print)
if ssid then
wifi.setmode(wifi.STATION, false)
wifi.sta.config { ssid = ssid, pwd = pwd, save = false }
end
tmr.alarm(0, 500, tmr.ALARM_AUTO, function()
if (wifi.sta.status() == wifi.STA_GOTIP) then
tmr.unregister(0)
print("Welcome to NodeMCU world", node.heap(), wifi.sta.getip())
return FTP.createServer(user, pass, dbgFlag)
else
uart.write(0,".")
end
end)
end
function FTP.createServer(user, pass, dbgFlag) -- upval: FTP (, debug, tostring, pcall, type, processCommand)
FTP.user, FTP.pass, FTP.debug = user, pass, dbgFlag
FTP.server = net.createServer(net.TCP, 180)
_G.FTP = FTP
debug("Server created: (userdata) %s", tostring(FTP.server))
FTP.server:listen(21, function(sock) -- upval: FTP (, debug, pcall, type, processCommand)
-- since a server can have multiple connections, each connection
-- has a CNX table to store connection-wide globals.
local client = FTP.client
local CNX; CNX = {
validUser = false,
cmdSocket = sock,
send = function(rec, cb) -- upval: CNX (,debug)
-- debug("Sending: %s", rec)
return CNX.cmdSocket:send(rec.."\r\n", cb)
end, --- send()
close = function(sock) -- upval: client, CNX (,debug, pcall, type)
-- debug("Closing CNX.socket=%s, sock=%s", tostring(CNX.socket), tostring(sock))
for _,s in ipairs{'cmdSocket', 'dataServer', 'dataSocket'} do
local sck; sck,CNX[s] = CNX[s], nil
-- debug("closing CNX.%s=%s", s, tostring(sck))
if type(sck)=='userdata' then pcall(sck.close, sck) end
end
client[sock] = nil
end -- CNX.close()
}
local function validateUser(sock, data) -- upval: CNX, FTP (, debug, processCommand)
-- validate the logon and if then switch to processing commands
-- debug("Authorising: %s", data)
local cmd, arg = data:match('([A-Za-z]+) *([^\r\n]*)')
local msg = "530 Not logged in, authorization required"
cmd = cmd:upper()
if cmd == 'USER' then
CNX.validUser = (arg == FTP.user)
msg = CNX.validUser and
"331 OK. Password required" or
"530 user not found"
elseif CNX.validUser and cmd == 'PASS' then
if arg == FTP.pass then
CNX.cwd = '/'
sock:on("receive", function(sock,data)
processCommand(CNX,sock,data)
end) -- logged on so switch to command mode
msg = "230 Login successful. Username & password correct; proceed."
else
msg = "530 Try again"
end
elseif cmd == 'AUTH' then
msg = "500 AUTH not understood"
end
return CNX.send(msg)
end
local port,ip = sock:getpeer()
-- debug("Connection accepted: (userdata) %s client %s:%u", tostring(sock), ip, port)
sock:on("receive", validateUser)
sock:on("disconnection", CNX.close)
FTP.client[sock]=CNX
CNX.send("220 FTP server ready");
end) -- FTP.server:listen()
end -- FTP.createServer()
function FTP.close() -- upval: FTP (, debug, post, tostring)
local svr = FTP.server
local function rollupClients(client, server) -- upval: FTP (,debug, post, tostring, rollupClients)
-- this is done recursively so that we only close one client per task
local skt,cxt = next(client)
if skt then
-- debug("Client close: %s", tostring(skt))
cxt.close(skt)
post(function() return rollupClients(client, server) end) -- upval: rollupClients, client, server
else
-- debug("Server close: %s", tostring(server))
server:close()
server:__gc()
FTP,_G.FTP = nil, nil -- the upval FTP can only be zeroed once FTP.client is cleared.
end
end
if svr then rollupClients(FTP.client, svr) end
package.loaded.ftpserver=nil
end -- FTP.close()
----------------------------- Process Command --------------------------------
-- This splits the valid commands into one of three categories:
-- * bare commands (which take no arg)
-- * simple commands (which take) a single arg; and
-- * data commands which initiate data transfer to or from the client and
-- hence need to use CBs.
--
-- Find strings are used do this lookup and minimise long if chains.
------------------------------------------------------------------------------
processCommand = function(cxt, sock, data) -- upvals: (, debug, processBareCmds, processSimpleCmds, processDataCmds)
debug("Command: %s", data)
data = data:gsub('[\r\n]+$', '') -- chomp trailing CRLF
local cmd, arg = data:match('([a-zA-Z]+) *(.*)')
cmd = cmd:upper()
local _cmd_ = '_'..cmd..'_'
if ('_CDUP_NOOP_PASV_PWD_QUIT_SYST_'):find(_cmd_) then
processBareCmds(cxt, cmd)
elseif ('_CWD_DELE_MODE_PORT_RNFR_RNTO_SIZE_TYPE_'):find(_cmd_) then
processSimpleCmds(cxt, cmd, arg)
elseif ('_LIST_NLST_RETR_STOR_'):find(_cmd_) then
processDataCmds(cxt, cmd, arg)
else
cxt.send("500 Unknown error")
end
end -- processCommand(sock, data)
-------------------------- Process Bare Commands -----------------------------
processBareCmds = function(cxt, cmd) -- upval: (dataServer)
local send = cxt.send
if cmd == 'CDUP' then
return send("250 OK. Current directory is "..cxt.cwd)
elseif cmd == 'NOOP' then
return send("200 OK")
elseif cmd == 'PASV' then
-- This FTP implementation ONLY supports PASV mode, and the passive port
-- listener is opened on receipt of the PASV command. If any data xfer
-- commands return an error if the PASV command hasn't been received.
-- Note the listener service is closed on receipt of the next PASV or
-- quit.
local ip, port, pphi, pplo, i1, i2, i3, i4, _
_,ip = cxt.cmdSocket:getaddr()
port = 2121
pplo = port % 256
pphi = (port-pplo)/256
i1,i2,i3,i4 = ip:match("(%d+).(%d+).(%d+).(%d+)")
dataServer(cxt, port)
return send(
('227 Entering Passive Mode(%d,%d,%d,%d,%d,%d)'):format(
i1,i2,i3,i4,pphi,pplo))
elseif cmd == 'PWD' then
return send('257 "/" is the current directory')
elseif cmd == 'QUIT' then
send("221 Goodbye", function() cxt.close(cxt.cmdSocket) end)
return
elseif cmd == 'SYST' then
-- return send("215 UNKNOWN")
return send("215 UNIX Type: L8") -- must be Unix so ls is parsed correctly
else
error('Oops. Missed '..cmd)
end
end -- processBareCmds(cmd, send)
------------------------- Process Simple Commands ----------------------------
local from -- needs to persist between simple commands
processSimpleCmds = function(cxt, cmd, arg) -- upval: from (, file, tostring, dataServer, debug)
local send = cxt.send
if cmd == 'MODE' then
return send(arg == "S" and "200 S OK" or
"504 Only S(tream) is suported")
elseif cmd == 'PORT' then
dataServer(cxt,nil) -- clear down any PASV setting
return send("502 Active mode not supported. PORT not implemented")
elseif cmd == 'TYPE' then
if arg == "A" then
cxt.xferType = 0
return send("200 TYPE is now ASII")
elseif arg == "I" then
cxt.xferType = 1
return send("200 TYPE is now 8-bit binary")
else
return send("504 Unknown TYPE")
end
end
-- The remaining commands take a filename as an arg. Strip off leading / and ./
arg = arg:gsub('^%.?/',''):gsub('^%.?/','')
debug("Filename is %s",arg)
if cmd == 'CWD' then
if arg:match('^[%./]*$') then
return send("250 CWD command successful")
end
return send("550 "..arg..": No such file or directory")
elseif cmd == 'DELE' then
if file.exists(arg) then
file.remove(arg)
if not file.exists(arg) then return send("250 Deleted "..arg) end
end
return send("550 Requested action not taken")
elseif cmd == 'RNFR' then
from = arg
send("350 RNFR accepted")
return
elseif cmd == 'RNTO' then
local status = from and file.rename(from, arg)
-- debug("rename('%s','%s')=%s", tostring(from), tostring(arg), tostring(status))
from = nil
return send(status and "250 File renamed" or
"550 Requested action not taken")
elseif cmd == "SIZE" then
local st = file.stat(arg)
return send(st and ("213 "..st.size) or
"550 Could not get file size.")
else
error('Oops. Missed '..cmd)
end
end -- processSimpleCmds(cmd, arg, send)
-------------------------- Process Data Commands -----------------------------
processDataCmds = function(cxt, cmd, arg) -- upval: FTP (, pairs, file, tostring, debug, post)
local send = cxt.send
-- The data commands are only accepted if a PORT command is in scope
if cxt.dataServer == nil and cxt.dataSocket == nil then
return send("502 Active mode not supported. "..cmd.." not implemented")
end
cxt.getData, cxt.setData = nil, nil
arg = arg:gsub('^%.?/',''):gsub('^%.?/','')
if cmd == "LIST" or cmd == "NLST" then
-- There are
local fileSize, nameList, pattern = file.list(), {}, '.'
arg = arg:gsub('^-[a-z]* *', '') -- ignore any Unix style command parameters
arg = arg:gsub('^/','') -- ignore any leading /
if #arg > 0 and arg ~= '.' then -- replace "*" by [^/%.]* that is any string not including / or .
pattern = arg:gsub('*','[^/%%.]*')
end
for k,v in pairs(fileSize) do
if k:match(pattern) then
nameList[#nameList+1] = k
else
fileSize[k] = nil
end
end
table.sort(nameList)
function cxt.getData() -- upval: cmd, fileSize, nameList (, table)
local list, user, v = {}, FTP.user
for i = 1,10 do
if #nameList == 0 then break end
local f = table.remove(nameList, 1)
list[#list+1] = (cmd == "LIST") and
("-rw-r--r-- 1 %s %s %6u Jan 1 00:00 %s\r\n"):format(user, user, fileSize[f], f) or
(f.."\r\n")
end
return table.concat(list)
end
elseif cmd == "RETR" then
local f = file.open(arg, "r")
if f then -- define a getter to read the file
function cxt.getData() -- upval: f
local buf = f:read(1024)
if not buf then f:close(); f = nil; end
return buf
end -- cxt.getData()
end
elseif cmd == "STOR" then
local f = file.open(arg, "w")
if f then -- define a setter to write the file
function cxt.setData(rec) -- upval f, arg (, debug)
-- debug("writing %u bytes to %s", #rec, arg)
return f:write(rec)
end -- cxt.saveData(rec)
function cxt.fileClose() -- upval cxt, f, arg (,debug)
-- debug("closing %s", arg)
f:close(); cxt.fileClose, f = nil, nil
end -- cxt.close()
end
end
send((cxt.getData or cxt.setData) and "150 Accepted data connection" or
"451 Can't open/create "..arg)
if cxt.getData and cxt.dataSocket then
debug ("poking sender to initiate first xfer")
post(function() cxt.sender(cxt.dataSocket) end)
end
end -- processDataCmds(cmd, arg, send)
----------------------------- Data Port Routines -----------------------------
-- These are used to manage the data transfer over the data port. This is
-- set up lazily either by a PASV or by the first LIST NLST RETR or STOR
-- command that uses it. These also provide a sendData / receiveData cb to
-- handle the actual xfer. Also note that the sending process can be primed in
--
---------------- Open a new data server and port ---------------------------
dataServer = function(cxt, n) -- upval: (pcall, net, ftpDataOpen, debug, tostring)
local dataServer = cxt.dataServer
if dataServer then -- close any existing listener
pcall(dataServer.close, dataServer)
end
if n then
-- Open a new listener if needed. Note that this is only used to establish
-- a single connection, so ftpDataOpen closes the server socket
cxt.dataServer = net.createServer(net.TCP, 300)
cxt.dataServer:listen(n, function(sock) -- upval: cxt, (ftpDataOpen)
ftpDataOpen(cxt,sock)
end)
-- debug("Listening on Data port %u, server %s",n, tostring(cxt.dataServer))
else
cxt.dataServer = nil
-- debug("Stopped listening on Data port",n)
end
end -- dataServer(n)
----------------------- Connection on FTP data port ------------------------
ftpDataOpen = function(cxt, dataSocket) -- upval: (debug, tostring, post, pcall)
local sport,sip = dataSocket:getaddr()
local cport,cip = dataSocket:getpeer()
debug("Opened data socket %s from %s:%u to %s:%u", tostring(dataSocket),sip,sport,cip,cport )
cxt.dataSocket = dataSocket
cxt.dataServer:close()
cxt.dataServer = nil
local function cleardown(skt,type) -- upval: cxt (, debug, tostring, post, pcall)
type = type==1 and "disconnection" or "reconnection"
local which = cxt.setData and "setData" or (cxt.getData and cxt.getData or "neither")
-- debug("Cleardown entered from %s with %s", type, which)
if cxt.setData then
cxt.fileClose()
cxt.setData = nil
cxt.send("226 Transfer complete.")
else
cxt.getData, cxt.sender = nil, nil
end
-- debug("Clearing down data socket %s", tostring(skt))
post(function() -- upval: skt, cxt, (, pcall)
pcall(skt.close, skt); skt=nil
cxt.dataSocket = nil
end)
end
local on_hold = false
dataSocket:on("receive", function(skt, rec) --upval: cxt, on_hold (, debug, tstring, post, node, pcall)
local which = cxt.setData and "setData" or (cxt.getData and cxt.getData or "neither")
-- debug("Received %u data bytes with %s", #rec, which)
if not cxt.setData then return end
if not on_hold then
-- Cludge to stop the client flooding the ESP SPIFFS on an upload of a
-- large file. As soon as a record arrives assert a flow control hold.
-- This can take up to 5 packets to come into effect at which point the
-- low priority unhold task is executed releasing the flow again.
-- debug("Issuing hold on data socket %s", tostring(skt))
skt:hold(); on_hold = true
post(node.task.LOW_PRIORITY,
function() -- upval: skt, on_hold (, debug, tostring))
-- debug("Issuing unhold on data socket %s", tostring(skt))
pcall(skt.unhold, skt); on_hold = false
end)
end
if not cxt.setData(rec) then
-- debug("Error writing to SPIFFS")
cxt.fileClose()
cxt.setData = nil
cxt.send("552 Upload aborted. Exceeded storage allocation")
end
end)
function cxt.sender(skt) -- upval: cxt (, debug)
debug ("entering sender")
if not cxt.getData then return end
local rec, skt = cxt.getData(), cxt.dataSocket
if rec and #rec > 0 then
-- debug("Sending %u data bytes", #rec)
skt:send(rec)
else
-- debug("Send of data completed")
skt:close()
cxt.send("226 Transfer complete.")
cxt.getData, cxt.dataSocket = nil, nil
end
end
dataSocket:on("sent", cxt.sender)
dataSocket:on("disconnection", function(skt) return cleardown(skt,1) end)
dataSocket:on("reconnection", function(skt) return cleardown(skt,2) end)
-- if we are sending to client then kick off the first send
if cxt.getData then cxt.sender(cxt.dataSocket) end
end -- ftpDataOpen(socket)
------------------------------------------------ -----------------------------
return FTP
...@@ -2,7 +2,8 @@ site_name: NodeMCU Documentation ...@@ -2,7 +2,8 @@ site_name: NodeMCU Documentation
site_description: Description of the NodeMCU documentation site_description: Description of the NodeMCU documentation
repo_url: https://github.com/nodemcu/nodemcu-firmware/ repo_url: https://github.com/nodemcu/nodemcu-firmware/
theme: readthedocs theme:
name: "readthedocs"
strict: true strict: true
markdown_extensions: markdown_extensions:
...@@ -24,14 +25,17 @@ pages: ...@@ -24,14 +25,17 @@ pages:
- Home: 'en/index.md' - Home: 'en/index.md'
- Building the firmware: 'en/build.md' - Building the firmware: 'en/build.md'
- Flashing the firmware: 'en/flash.md' - Flashing the firmware: 'en/flash.md'
- Internal filesystem notes: 'en/spiffs.md'
- Filesystem on SD card: 'en/sdcard.md'
- Uploading code: 'en/upload.md' - Uploading code: 'en/upload.md'
- Support: 'en/support.md'
- FAQs: - FAQs:
- Lua Developer FAQ: 'en/lua-developer-faq.md' - Lua Developer FAQ: 'en/lua-developer-faq.md'
- Extension Developer FAQ: 'en/extn-developer-faq.md' - Extension Developer FAQ: 'en/extn-developer-faq.md'
- Hardware FAQ: 'en/hardware-faq.md' - Hardware FAQ: 'en/hardware-faq.md'
- Support: 'en/support.md' - Whitepapers:
- Filesystem on SD card: 'en/sdcard.md'
- Internal filesystem: 'en/spiffs.md'
- Lua Compact Debug (LCD): 'en/lcd.md'
- Lua Flash Store (LFS): 'en/lfs.md'
- Modules: - Modules:
- 'adc': 'en/modules/adc.md' - 'adc': 'en/modules/adc.md'
- 'ads1115' : 'en/modules/ads1115.md' - 'ads1115' : 'en/modules/ads1115.md'
...@@ -90,7 +94,7 @@ pages: ...@@ -90,7 +94,7 @@ pages:
- 'tm1829': 'en/modules/tm1829.md' - 'tm1829': 'en/modules/tm1829.md'
- 'tmr': 'en/modules/tmr.md' - 'tmr': 'en/modules/tmr.md'
- 'tsl2561': 'en/modules/tsl2561.md' - 'tsl2561': 'en/modules/tsl2561.md'
- 'u8g': 'en/modules/u8g.md' - 'u8g2': 'en/modules/u8g2.md'
- 'uart': 'en/modules/uart.md' - 'uart': 'en/modules/uart.md'
- 'ucg': 'en/modules/ucg.md' - 'ucg': 'en/modules/ucg.md'
- 'websocket': 'en/modules/websocket.md' - 'websocket': 'en/modules/websocket.md'
......
################################################################# #################################################################
# This file is configured in RTD -> Admin -> Advanced Settings! # # This file is configured in RTD -> Admin -> Advanced Settings! #
################################################################# #################################################################
# RTD - MkDocs integration is broken for # Enforce a specific MkDocs version by using the standard pip requirements.txt syntax
# - MkDocs >= 0.17 due to in-site search failures: https://github.com/rtfd/readthedocs.org/issues/3174
# - MkDocs >= 0.16 due to stuck fly-out menu: https://groups.google.com/d/msg/mkdocs/v7AVbeB105w/FIlE_n2-AgAJ
# mkdocs >= 0.16.3, < 0.17 # mkdocs >= 0.16.3, < 0.17
...@@ -2,10 +2,11 @@ ...@@ -2,10 +2,11 @@
# Options # Options
# #
FSSOURCE ?= ../local/fs/ FSSOURCE ?= ../local/fs
LUASOURCE ?= ../local/lua
FLASHSIZE ?= 4mb 32mb 8mb FLASHSIZE ?= 4mb 32mb 8mb
FLASH_SW = -S
SUBDIRS = SUBDIRS =
HOSTCC ?= gcc
OBJDUMP = $(or $(shell which objdump),xtensa-lx106-elf-objdump) OBJDUMP = $(or $(shell which objdump),xtensa-lx106-elf-objdump)
...@@ -13,14 +14,34 @@ OBJDUMP = $(or $(shell which objdump),xtensa-lx106-elf-objdump) ...@@ -13,14 +14,34 @@ OBJDUMP = $(or $(shell which objdump),xtensa-lx106-elf-objdump)
# Get the files to pack into the spiffs image # Get the files to pack into the spiffs image
# #
SPIFFSFILES ?= $(patsubst $(FSSOURCE)%,%,$(shell find $(FSSOURCE) -name '*' '!' -name .gitignore )) SPIFFSFILES ?= $(patsubst $(FSSOURCE)/%,%,$(shell find $(FSSOURCE)/ -name '*' '!' -name .gitignore ))
################################################################# #################################################################
# Get the filesize of /bin/0x10000.bin # Get the filesize of /bin/0x10000.bin and SPIFFS sizing
# #
FLASH_USED_END = $$((0x`$(OBJDUMP) -t ../app/.output/eagle/debug/image/eagle.app.v6.out |grep _flash_used_end |cut -f1 -d" "` - 0x40200000)) FLASH_FS_SIZE := $(shell $(CC) -E -dM - <../app/include/user_config.h | grep SPIFFS_MAX_FILESYSTEM_SIZE| cut -d ' ' -f 3)
ifneq ($(strip $(FLASH_FS_SIZE)),)
FLASHSIZE = $(shell printf "0x%x" $(FLASH_FS_SIZE))
FLASH_SW = -c
endif
FLASH_FS_LOC := $(shell $(CC) -E -dM - <../app/include/user_config.h | grep SPIFFS_FIXED_LOCATION| cut -d ' ' -f 3)
ifeq ($(strip $(FLASH_FS_LOC)),)
FLASH_FS_LOC := $(shell printf "0x%x" $$((0x$(shell $(OBJDUMP) -t ../app/.output/eagle/debug/image/eagle.app.v6.out |grep " _flash_used_end" |cut -f1 -d" ") - 0x40200000)))
else
FLASH_FS_LOC := $(shell printf "0x%x" $(FLASH_FS_LOC))
endif
LFSSOURCES := $(wildcard $(LUASOURCE)/*.lua)
BUILD_TYPE := $(shell $(CC) $(EXTRA_CCFLAGS) -E -dM - <../app/include/user_config.h | grep LUA_NUMBER_INTEGRAL | wc -l)
ifeq ($(BUILD_TYPE),0)
LUAC_CROSS := ../luac.cross
else
LUAC_CROSS := ../luac.cross.int
endif
############################################################# #############################################################
# Rules base # Rules base
...@@ -29,6 +50,16 @@ FLASH_USED_END = $$((0x`$(OBJDUMP) -t ../app/.output/eagle/debug/image/eagle.app ...@@ -29,6 +50,16 @@ FLASH_USED_END = $$((0x`$(OBJDUMP) -t ../app/.output/eagle/debug/image/eagle.app
all: spiffsscript all: spiffsscript
.PHONY: TEST
TEST:
@echo $(FLASHSIZE)
@echo $(FLASH_FS_SIZE)
@echo $(FLASH_FS_LOC)
@echo $(FLASH_USED_END)
spiffsimg/spiffsimg:
.PHONY: spiffsimg .PHONY: spiffsimg
.PHONY: spiffsimg/spiffsimg .PHONY: spiffsimg/spiffsimg
...@@ -37,15 +68,23 @@ spiffsimg: spiffsimg/spiffsimg ...@@ -37,15 +68,23 @@ spiffsimg: spiffsimg/spiffsimg
@echo Built spiffsimg in spiffsimg/spiffsimg @echo Built spiffsimg in spiffsimg/spiffsimg
spiffsimg/spiffsimg: spiffsimg/spiffsimg:
@$(MAKE) -C spiffsimg CC=$(HOSTCC) @$(MAKE) -C spiffsimg
spiffsscript: remove-image spiffsimg/spiffsimg spiffsscript: remove-image LFSimage spiffsimg/spiffsimg
rm -f ./spiffsimg/spiffs.lst rm -f ./spiffsimg/spiffs.lst
echo "" >> ./spiffsimg/spiffs.lst @echo "" >> ./spiffsimg/spiffs.lst
@$(foreach f, $(SPIFFSFILES), echo "import $(FSSOURCE)$(f) $(f)" >> ./spiffsimg/spiffs.lst ;) @$(foreach f, $(SPIFFSFILES), echo "import $(FSSOURCE)/$(f) $(f)" >> ./spiffsimg/spiffs.lst ;)
@$(foreach sz, $(FLASHSIZE), spiffsimg/spiffsimg -U $(FLASH_USED_END) -o ../bin/spiffs-$(sz).dat -f ../bin/0x%x-$(sz).bin -S $(sz) -r ./spiffsimg/spiffs.lst -d; ) $(foreach sz, $(FLASHSIZE), spiffsimg/spiffsimg -f ../bin/0x%x-$(sz).img $(FLASH_SW) $(sz) -U $(FLASH_FS_LOC) -r ./spiffsimg/spiffs.lst -d; )
@$(foreach sz, $(FLASHSIZE), if [ -r ../bin/spiffs-$(sz).dat ]; then echo Built $$(cat ../bin/spiffs-$(sz).dat)-$(sz).bin; fi; ) @$(foreach sz, $(FLASHSIZE), if [ -r ../bin/spiffs-$(sz).dat ]; then echo Built $$(cat ../bin/spiffs-$(sz).dat)-$(sz).bin; fi; )
ifneq ($(LFSSOURCES),)
LFSimage: $(LFSSOURCES)
$(LUAC_CROSS) -f -o $(FSSOURCE)/LFS.img $(LFSSOURCES)
else
LFSimage:
rm -f $(FSSOURCE)/LFS.img
endif
remove-image: remove-image:
$(foreach sz, $(FLASHSIZE), if [ -r ../bin/spiffs-$(sz).dat ]; then rm -f ../bin/$$(cat ../bin/spiffs-$(sz).dat)-$(sz).bin; fi; ) $(foreach sz, $(FLASHSIZE), if [ -r ../bin/spiffs-$(sz).dat ]; then rm -f ../bin/$$(cat ../bin/spiffs-$(sz).dat)-$(sz).bin; fi; )
rm -f ../bin/spiffs*.dat rm -f ../bin/spiffs*.dat
......
-- eLua build system
module( ..., package.seeall )
local lfs = require "lfs"
local sf = string.format
utils = require "tools.utils"
-------------------------------------------------------------------------------
-- Various helpers
-- Return the time of the last modification of the file
local function get_ftime( path )
local t = lfs.attributes( path, 'modification' )
return t or -1
end
-- Check if a given target name is phony
local function is_phony( target )
return target:find( "#phony" ) == 1
end
-- Return a string with $(key) replaced with 'value'
local function expand_key( s, key, value )
if not value then return s end
local fmt = sf( "%%$%%(%s%%)", key )
return ( s:gsub( fmt, value ) )
end
-- Return a target name considering phony targets
local function get_target_name( s )
if not is_phony( s ) then return s end
end
-- 'Liniarize' a file name by replacing its path separators indicators with '_'
local function linearize_fname( s )
return ( s:gsub( "[\\/]", "__" ) )
end
-- Helper: transform a table into a string if needed
local function table_to_string( t )
if not t then return nil end
if type( t ) == "table" then t = table.concat( t, " " ) end
return t
end
-- Helper: return the extended type of an object (takes into account __type)
local function exttype( o )
local t = type( o )
if t == "table" and o.__type then t = o:__type() end
return t
end
---------------------------------------
-- Table utils
-- (from http://lua-users.org/wiki/TableUtils)
function table.val_to_str( v )
if "string" == type( v ) then
v = string.gsub( v, "\n", "\\n" )
if string.match( string.gsub(v,"[^'\"]",""), '^"+$' ) then
return "'" .. v .. "'"
end
return '"' .. string.gsub(v,'"', '\\"' ) .. '"'
else
return "table" == type( v ) and table.tostring( v ) or tostring( v )
end
end
function table.key_to_str ( k )
if "string" == type( k ) and string.match( k, "^[_%a][_%a%d]*$" ) then
return k
else
return "[" .. table.val_to_str( k ) .. "]"
end
end
function table.tostring( tbl )
local result, done = {}, {}
for k, v in ipairs( tbl ) do
table.insert( result, table.val_to_str( v ) )
done[ k ] = true
end
for k, v in pairs( tbl ) do
if not done[ k ] then
table.insert( result,
table.key_to_str( k ) .. "=" .. table.val_to_str( v ) )
end
end
return "{" .. table.concat( result, "," ) .. "}"
end
-------------------------------------------------------------------------------
-- Dummy 'builder': simply checks the date of a file
local _fbuilder = {}
_fbuilder.new = function( target, dep )
local self = {}
setmetatable( self, { __index = _fbuilder } )
self.target = target
self.dep = dep
return self
end
_fbuilder.build = function( self )
-- Doesn't build anything but returns 'true' if the dependency is newer than
-- the target
if is_phony( self.target ) then
return true
else
return get_ftime( self.dep ) > get_ftime( self.target )
end
end
_fbuilder.target_name = function( self )
return get_target_name( self.dep )
end
-- Object type
_fbuilder.__type = function()
return "_fbuilder"
end
-------------------------------------------------------------------------------
-- Target object
local _target = {}
_target.new = function( target, dep, command, builder, ttype )
local self = {}
setmetatable( self, { __index = _target } )
self.target = target
self.command = command
self.builder = builder
builder:register_target( target, self )
self:set_dependencies( dep )
self.dep = self:_build_dependencies( self.origdep )
self.dont_clean = false
self.can_substitute_cmdline = false
self._force_rebuild = #self.dep == 0
builder.runlist[ target ] = false
self:set_type( ttype )
return self
end
-- Set dependencies as a string; actual dependencies are computed by _build_dependencies
-- (below) when 'build' is called
_target.set_dependencies = function( self, dep )
self.origdep = dep
end
-- Set the target type
-- This is only for displaying actions
_target.set_type = function( self, ttype )
local atable = { comp = { "[COMPILE]", 'blue' } , dep = { "[DEPENDS]", 'magenta' }, link = { "[LINK]", 'yellow' }, asm = { "[ASM]", 'white' } }
local tdata = atable[ ttype ]
if not tdata then
self.dispstr = is_phony( self.target ) and "[PHONY]" or "[TARGET]"
self.dispcol = 'green'
else
self.dispstr = tdata[ 1 ]
self.dispcol = tdata[ 2 ]
end
end
-- Set dependencies
-- This uses a proxy table and returns string deps dynamically according
-- to the targets currently registered in the builder
_target._build_dependencies = function( self, dep )
-- Step 1: start with an array
if type( dep ) == "string" then dep = utils.string_to_table( dep ) end
-- Step 2: linearize "dep" array keeping targets
local filter = function( e )
local t = exttype( e )
return t ~= "_ftarget" and t ~= "_target"
end
dep = utils.linearize_array( dep, filter )
-- Step 3: strings are turned into _fbuilder objects if not found as targets;
-- otherwise the corresponding target object is used
for i = 1, #dep do
if type( dep[ i ] ) == 'string' then
local t = self.builder:get_registered_target( dep[ i ] )
dep[ i ] = t or _fbuilder.new( self.target, dep[ i ] )
end
end
return dep
end
-- Set pre-build function
_target.set_pre_build_function = function( self, f )
self._pre_build_function = f
end
-- Set post-build function
_target.set_post_build_function = function( self, f )
self._post_build_function = f
end
-- Force rebuild
_target.force_rebuild = function( self, flag )
self._force_rebuild = flag
end
-- Set additional arguments to send to the builder function if it is a callable
_target.set_target_args = function( self, args )
self._target_args = args
end
-- Function to execute in clean mode
_target._cleaner = function( target, deps, tobj, disp_mode )
-- Clean the main target if it is not a phony target
local dprint = function( ... )
if disp_mode ~= "minimal" then print( ... ) end
end
if not is_phony( target ) then
if tobj.dont_clean then
dprint( sf( "[builder] Target '%s' will not be deleted", target ) )
return 0
end
if disp_mode ~= "minimal" then io.write( sf( "[builder] Removing %s ... ", target ) ) end
if os.remove( target ) then dprint "done." else dprint "failed!" end
end
return 0
end
-- Build the given target
_target.build = function( self )
if self.builder.runlist[ self.target ] then return end
local docmd = self:target_name() and lfs.attributes( self:target_name(), "mode" ) ~= "file"
docmd = docmd or self.builder.global_force_rebuild
local initdocmd = docmd
self.dep = self:_build_dependencies( self.origdep )
local depends, dep, previnit = '', self.dep, self.origdep
-- Iterate through all dependencies, execute each one in turn
local deprunner = function()
for i = 1, #dep do
local res = dep[ i ]:build()
docmd = docmd or res
local t = dep[ i ]:target_name()
if exttype( dep[ i ] ) == "_target" and t and not is_phony( self.target ) then
docmd = docmd or get_ftime( t ) > get_ftime( self.target )
end
if t then depends = depends .. t .. " " end
end
end
deprunner()
-- Execute the preb-build function if needed
if self._pre_build_function then self._pre_build_function( self, docmd ) end
-- If the dependencies changed as a result of running the pre-build function
-- run through them again
if previnit ~= self.origdep then
self.dep = self:_build_dependencies( self.origdep )
depends, dep, docmd = '', self.dep, initdocmd
deprunner()
end
-- If at least one dependency is new rebuild the target
docmd = docmd or self._force_rebuild or self.builder.clean_mode
local keep_flag = true
if docmd and self.command then
if self.builder.disp_mode ~= 'all' and self.builder.disp_mode ~= "minimal" and not self.builder.clean_mode then
io.write( utils.col_funcs[ self.dispcol ]( self.dispstr ) .. " " )
end
local cmd, code = self.command
if self.builder.clean_mode then cmd = _target._cleaner end
if type( cmd ) == 'string' then
cmd = expand_key( cmd, "TARGET", self.target )
cmd = expand_key( cmd, "DEPENDS", depends )
cmd = expand_key( cmd, "FIRST", dep[ 1 ]:target_name() )
if self.builder.disp_mode == 'all' then
print( cmd )
elseif self.builder.disp_mode ~= "minimal" then
print( self.target )
end
code = self:execute( cmd )
else
if not self.builder.clean_mode and self.builder.disp_mode ~= "all" and self.builder.disp_mode ~= "minimal" then
print( self.target )
end
code = cmd( self.target, self.dep, self.builder.clean_mode and self or self._target_args, self.builder.disp_mode )
if code == 1 then -- this means "mark target as 'not executed'"
keep_flag = false
code = 0
end
end
if code ~= 0 then
print( utils.col_red( "[builder] Error building target" ) )
if self.builder.disp_mode ~= 'all' and type( cmd ) == "string" then
print( utils.col_red( "[builder] Last executed command was: " ) )
print( cmd )
end
os.exit( 1 )
end
end
-- Execute the post-build function if needed
if self._post_build_function then self._post_build_function( self, docmd ) end
-- Marked target as "already ran" so it won't run again
self.builder.runlist[ self.target ] = true
return docmd and keep_flag
end
-- Return the actual target name (taking into account phony targets)
_target.target_name = function( self )
return get_target_name( self.target )
end
-- Restrict cleaning this target
_target.prevent_clean = function( self, flag )
self.dont_clean = flag
end
-- Object type
_target.__type = function()
return "_target"
end
_target.execute = function( self, cmd )
local code
if utils.is_windows() and #cmd > 8190 and self.can_substitute_cmdline then
-- Avoid cmd's maximum command line length limitation
local t = cmd:find( " " )
f = io.open( "tmpcmdline", "w" )
local rest = cmd:sub( t + 1 )
f:write( ( rest:gsub( "\\", "/" ) ) )
f:close()
cmd = cmd:sub( 1, t - 1 ) .. " @tmpcmdline"
end
local code = os.execute( cmd )
os.remove( "tmpcmdline" )
return code
end
_target.set_substitute_cmdline = function( self, flag )
self.can_substitute_cmdline = flag
end
-------------------------------------------------------------------------------
-- Builder public interface
builder = { KEEP_DIR = 0, BUILD_DIR_LINEARIZED = 1 }
---------------------------------------
-- Initialization and option handling
-- Create a new builder object with the output in 'build_dir' and with the
-- specified compile, dependencies and link command
builder.new = function( build_dir )
self = {}
setmetatable( self, { __index = builder } )
self.build_dir = build_dir or ".build"
self.exe_extension = utils.is_windows() and "exe" or ""
self.clean_mode = false
self.opts = utils.options_handler()
self.args = {}
self.user_args = {}
self.build_mode = self.KEEP_DIR
self.targets = {}
self.targetargs = {}
self._tlist = {}
self.runlist = {}
self.disp_mode = 'all'
self.cmdline_macros = {}
self.c_targets = {}
self.preprocess_mode = false
self.asm_mode = false
return self
end
-- Helper: create the build output directory
builder._create_build_dir = function( self )
if self.build_dir_created then return end
if self.build_mode ~= self.KEEP_DIR then
-- Create builds directory if needed
local mode = lfs.attributes( self.build_dir, "mode" )
if not mode or mode ~= "directory" then
if not utils.full_mkdir( self.build_dir ) then
print( "[builder] Unable to create directory " .. self.build_dir )
os.exit( 1 )
end
end
end
self.build_dir_created = true
end
-- Add an options to the builder
builder.add_option = function( self, name, help, default, data )
self.opts:add_option( name, help, default, data )
end
-- Initialize builder from the given command line
builder.init = function( self, args )
-- Add the default options
local opts = self.opts
opts:add_option( "build_mode", 'choose location of the object files', self.KEEP_DIR,
{ keep_dir = self.KEEP_DIR, build_dir_linearized = self.BUILD_DIR_LINEARIZED } )
opts:add_option( "build_dir", 'choose build directory', self.build_dir )
opts:add_option( "disp_mode", 'set builder display mode', 'summary', { 'all', 'summary', 'minimal' } )
-- Apply default values to all options
for i = 1, opts:get_num_opts() do
local o = opts:get_option( i )
self.args[ o.name:upper() ] = o.default
end
-- Read and interpret command line
for i = 1, #args do
local a = args[ i ]
if a:upper() == "-C" then -- clean option (-c)
self.clean_mode = true
elseif a:upper() == '-H' then -- help option (-h)
self:_show_help()
os.exit( 1 )
elseif a:upper() == "-E" then -- preprocess
self.preprocess_mode = true
elseif a:upper() == "-S" then -- generate assembler
self.asm_mode = true
elseif a:find( '-D' ) == 1 and #a > 2 then -- this is a macro definition that will be auomatically added to the compiler flags
table.insert( self.cmdline_macros, a:sub( 3 ) )
elseif a:find( '=' ) then -- builder argument (key=value)
local k, v = opts:handle_arg( a )
if not k then
self:_show_help()
os.exit( 1 )
end
self.args[ k:upper() ] = v
self.user_args[ k:upper() ] = true
else -- this must be the target name / target arguments
if self.targetname == nil then
self.targetname = a
else
table.insert( self.targetargs, a )
end
end
end
-- Read back the default options
self.build_mode = self.args.BUILD_MODE
self.build_dir = self.args.BUILD_DIR
self.disp_mode = self.args.DISP_MODE
end
-- Return the value of the option with the given name
builder.get_option = function( self, optname )
return self.args[ optname:upper() ]
end
-- Returns true if the given option was specified by the user on the command line, false otherwise
builder.is_user_option = function( self, optname )
return self.user_args[ optname:upper() ]
end
-- Show builder help
builder._show_help = function( self )
print( "[builder] Valid options:" )
print( " -h: help (this text)" )
print( " -c: clean target" )
print( " -E: generate preprocessed output for single file targets" )
print( " -S: generate assembler output for single file targets" )
self.opts:show_help()
end
---------------------------------------
-- Builder configuration
-- Set the compile command
builder.set_compile_cmd = function( self, cmd )
self.comp_cmd = cmd
end
-- Set the link command
builder.set_link_cmd = function( self, cmd )
self.link_cmd = cmd
end
-- Set the assembler command
builder.set_asm_cmd = function( self, cmd )
self._asm_cmd = cmd
end
-- Set (actually force) the object file extension
builder.set_object_extension = function( self, ext )
self.obj_extension = ext
end
-- Set (actually force) the executable file extension
builder.set_exe_extension = function( self, ext )
self.exe_extension = ext
end
-- Set the clean mode
builder.set_clean_mode = function( self, isclean )
self.clean_mode = isclean
end
-- Sets the build mode
builder.set_build_mode = function( self, mode )
self.build_mode = mode
end
-- Set the build directory
builder.set_build_dir = function( self, dir )
if self.build_dir_created then
print "[builder] Error: build directory already created"
os.exit( 1 )
end
self.build_dir = dir
self:_create_build_dir()
end
-- Return the current build directory
builder.get_build_dir = function( self )
return self.build_dir
end
-- Return the target arguments
builder.get_target_args = function( self )
return self.targetargs
end
-- Set a specific dependency generation command for the assembler
-- Pass 'false' to skip dependency generation for assembler files
builder.set_asm_dep_cmd = function( self, asm_dep_cmd )
self.asm_dep_cmd = asm_dep_cmd
end
-- Set a specific dependency generation command for the compiler
-- Pass 'false' to skip dependency generation for C files
builder.set_c_dep_cmd = function( self, c_dep_cmd )
self.c_dep_cmd = c_dep_cmd
end
-- Save the builder configuration for a given component to a string
builder._config_to_string = function( self, what )
local ctable = {}
local state_fields
if what == 'comp' then
state_fields = { 'comp_cmd', '_asm_cmd', 'c_dep_cmd', 'asm_dep_cmd', 'obj_extension' }
elseif what == 'link' then
state_fields = { 'link_cmd' }
else
print( sf( "Invalid argument '%s' to _config_to_string", what ) )
os.exit( 1 )
end
utils.foreach( state_fields, function( k, v ) ctable[ v ] = self[ v ] end )
return table.tostring( ctable )
end
-- Check the configuration of the given component against the previous one
-- Return true if the configuration has changed
builder._compare_config = function( self, what )
local res = false
local crtstate = self:_config_to_string( what )
if not self.clean_mode then
local fconf = io.open( self.build_dir .. utils.dir_sep .. ".builddata." .. what, "rb" )
if fconf then
local oldstate = fconf:read( "*a" )
fconf:close()
if oldstate:lower() ~= crtstate:lower() then res = true end
end
end
-- Write state to build dir
fconf = io.open( self.build_dir .. utils.dir_sep .. ".builddata." .. what, "wb" )
if fconf then
fconf:write( self:_config_to_string( what ) )
fconf:close()
end
return res
end
-- Sets the way commands are displayed
builder.set_disp_mode = function( self, mode )
mode = mode:lower()
if mode ~= 'all' and mode ~= 'summary' and mode ~= "minimal" then
print( sf( "[builder] Invalid display mode '%s'", mode ) )
os.exit( 1 )
end
self.disp_mode = mode
end
---------------------------------------
-- Command line builders
-- Internal helper
builder._generic_cmd = function( self, args )
local compcmd = args.compiler or "gcc"
compcmd = compcmd .. " "
local flags = type( args.flags ) == 'table' and table_to_string( utils.linearize_array( args.flags ) ) or args.flags
local defines = type( args.defines ) == 'table' and table_to_string( utils.linearize_array( args.defines ) ) or args.defines
local includes = type( args.includes ) == 'table' and table_to_string( utils.linearize_array( args.includes ) ) or args.includes
local comptype = table_to_string( args.comptype ) or "-c"
compcmd = compcmd .. utils.prepend_string( defines, "-D" )
compcmd = compcmd .. utils.prepend_string( includes, "-I" )
return compcmd .. flags .. " " .. comptype .. " -o $(TARGET) $(FIRST)"
end
-- Return a compile command based on the specified args
builder.compile_cmd = function( self, args )
args.defines = { args.defines, self.cmdline_macros }
if self.preprocess_mode then
args.comptype = "-E"
elseif self.asm_mode then
args.comptype = "-S"
else
args.comptype = "-c"
end
return self:_generic_cmd( args )
end
-- Return an assembler command based on the specified args
builder.asm_cmd = function( self, args )
args.defines = { args.defines, self.cmdline_macros }
args.compiler = args.assembler
args.comptype = self.preprocess_mode and "-E" or "-c"
return self:_generic_cmd( args )
end
-- Return a link command based on the specified args
builder.link_cmd = function( self, args )
local flags = type( args.flags ) == 'table' and table_to_string( utils.linearize_array( args.flags ) ) or args.flags
local libraries = type( args.libraries ) == 'table' and table_to_string( utils.linearize_array( args.libraries ) ) or args.libraries
local linkcmd = args.linker or "gcc"
linkcmd = linkcmd .. " " .. flags .. " -o $(TARGET) $(DEPENDS)"
linkcmd = linkcmd .. " " .. utils.prepend_string( libraries, "-l" )
return linkcmd
end
---------------------------------------
-- Target handling
-- Create a return a new C to object target
builder.c_target = function( self, target, deps, comp_cmd )
return _target.new( target, deps, comp_cmd or self.comp_cmd, self, 'comp' )
end
-- Create a return a new ASM to object target
builder.asm_target = function( self, target, deps, asm_cmd )
return _target.new( target, deps, asm_cmd or self._asm_cmd, self, 'asm' )
end
-- Return the name of a dependency file name corresponding to a C source
builder.get_dep_filename = function( self, srcname )
return utils.replace_extension( self.build_dir .. utils.dir_sep .. linearize_fname( srcname ), "d" )
end
-- Create a return a new C dependency target
builder.dep_target = function( self, dep, depdeps, dep_cmd )
local depname = self:get_dep_filename( dep )
return _target.new( depname, depdeps, dep_cmd, self, 'dep' )
end
-- Create and return a new link target
builder.link_target = function( self, out, dep, link_cmd )
local path, ext = utils.split_ext( out )
if not ext and self.exe_extension and #self.exe_extension > 0 then
out = out .. self.exe_extension
end
local t = _target.new( out, dep, link_cmd or self.link_cmd, self, 'link' )
if self:_compare_config( 'link' ) then t:force_rebuild( true ) end
t:set_substitute_cmdline( true )
return t
end
-- Create and return a new generic target
builder.target = function( self, dest_target, deps, cmd )
return _target.new( dest_target, deps, cmd, self )
end
-- Register a target (called from _target.new)
builder.register_target = function( self, name, obj )
self._tlist[ name:gsub( "\\", "/" ) ] = obj
end
-- Returns a registered target (nil if not found)
builder.get_registered_target = function( self, name )
return self._tlist[ name:gsub( "\\", "/" ) ]
end
---------------------------------------
-- Actual building functions
-- Return the object name corresponding to a source file name
builder.obj_name = function( self, name, ext )
local r = ext or self.obj_extension
if not r then
r = utils.is_windows() and "obj" or "o"
end
local objname = utils.replace_extension( name, r )
-- KEEP_DIR: object file in the same directory as source file
-- BUILD_DIR_LINEARIZED: object file in the build directory, linearized filename
if self.build_mode == self.KEEP_DIR then
return objname
elseif self.build_mode == self.BUILD_DIR_LINEARIZED then
return self.build_dir .. utils.dir_sep .. linearize_fname( objname )
end
end
-- Read and interpret dependencies for each file specified in "ftable"
-- "ftable" is either a space-separated string with all the source files or an array
builder.read_depends = function( self, ftable )
if type( ftable ) == 'string' then ftable = utils.string_to_table( ftable ) end
-- Read dependency data
local dtable = {}
for i = 1, #ftable do
local f = io.open( self:get_dep_filename( ftable[ i ] ), "rb" )
local lines = ftable[ i ]
if f then
lines = f:read( "*a" )
f:close()
lines = lines:gsub( "\n", " " ):gsub( "\\%s+", " " ):gsub( "%s+", " " ):gsub( "^.-: (.*)", "%1" )
end
dtable[ ftable[ i ] ] = lines
end
return dtable
end
-- Create and return compile targets for the given sources
builder.create_compile_targets = function( self, ftable, res )
if type( ftable ) == 'string' then ftable = utils.string_to_table( ftable ) end
res = res or {}
ccmd, oname = "-c", "o"
if self.preprocess_mode then
ccmd, oname = '-E', "pre"
elseif self.asm_mode then
ccmd, oname = '-S', 's'
end
-- Build dependencies for all targets
for i = 1, #ftable do
local isasm = ftable[ i ]:find( "%.c$" ) == nil
-- Skip assembler targets if 'asm_dep_cmd' is set to 'false'
-- Skip C targets if 'c_dep_cmd' is set to 'false'
local skip = isasm and self.asm_dep_cmd == false
skip = skip or ( not isasm and self.c_dep_cmd == false )
local deps = self:get_dep_filename( ftable[ i ] )
local target
if not isasm then
local depcmd = skip and self.comp_cmd or ( self.c_dep_cmd or self.comp_cmd:gsub( ccmd .. " ", sf( ccmd .. " -MD -MF %s ", deps ) ) )
target = self:c_target( self:obj_name( ftable[ i ], oname ), { self:get_registered_target( deps ) or ftable[ i ] }, depcmd )
else
local depcmd = skip and self._asm_cmd or ( self.asm_dep_cmd or self._asm_cmd:gsub( ccmd .. " ", sf( ccmd .. " -MD -MF %s ", deps ) ) )
target = self:asm_target( self:obj_name( ftable[ i ], oname ), { self:get_registered_target( deps ) or ftable[ i ] }, depcmd )
end
-- Pre build step: replace dependencies with the ones from the compiler generated dependency file
local dprint = function( ... ) if self.disp_mode ~= "minimal" then print( ... ) end end
if not skip then
target:set_pre_build_function( function( t, _ )
if not self.clean_mode then
local fres = self:read_depends( ftable[ i ] )
local fdeps = fres[ ftable[ i ] ]
if #fdeps:gsub( "%s+", "" ) == 0 then fdeps = ftable[ i ] end
t:set_dependencies( fdeps )
else
if self.disp_mode ~= "minimal" then io.write( sf( "[builder] Removing %s ... ", deps ) ) end
if os.remove( deps ) then dprint "done." else dprint "failed!" end
end
end )
end
target.srcname = ftable[ i ]
-- TODO: check clean mode?
if not isasm then self.c_targets[ #self.c_targets + 1 ] = target end
table.insert( res, target )
end
return res
end
-- Add a target to the list of builder targets
builder.add_target = function( self, target, help, alias )
self.targets[ target.target ] = { target = target, help = help }
alias = alias or {}
for _, v in ipairs( alias ) do
self.targets[ v ] = { target = target, help = help }
end
return target
end
-- Make a target the default one
builder.default = function( self, target )
self.deftarget = target.target
self.targets.default = { target = target, help = "default target" }
end
-- Build everything
builder.build = function( self, target )
local t = self.targetname or self.deftarget
if not t then
print( utils.col_red( "[builder] Error: build target not specified" ) )
os.exit( 1 )
end
local trg
-- Look for single targets (C source files)
for _, ct in pairs( self.c_targets ) do
if ct.srcname == t then
trg = ct
break
end
end
if not trg then
if not self.targets[ t ] then
print( sf( "[builder] Error: target '%s' not found", t ) )
print( "Available targets: " )
print( " all source files" )
for k, v in pairs( self.targets ) do
if not is_phony( k ) then
print( sf( " %s - %s", k, v.help or "(no help available)" ) )
end
end
if self.deftarget and not is_phony( self.deftarget ) then
print( sf( "Default target is '%s'", self.deftarget ) )
end
os.exit( 1 )
else
if self.preprocess_mode or self.asm_mode then
print( "[builder] Error: preprocess (-E) or asm (-S) works only with single file targets." )
os.exit( 1 )
end
trg = self.targets[ t ].target
end
end
self:_create_build_dir()
-- At this point check if we have a change in the state that would require a rebuild
if self:_compare_config( 'comp' ) then
print( utils.col_yellow( "[builder] Forcing rebuild due to configuration change." ) )
self.global_force_rebuild = true
else
self.global_force_rebuild = false
end
-- Do the actual build
local res = trg:build()
if not res then print( utils.col_yellow( sf( '[builder] %s: up to date', t ) ) ) end
if self.clean_mode then
os.remove( self.build_dir .. utils.dir_sep .. ".builddata.comp" )
os.remove( self.build_dir .. utils.dir_sep .. ".builddata.link" )
end
print( utils.col_yellow( "[builder] Done building target." ) )
return res
end
-- Create dependencies, create object files, link final object
builder.make_exe_target = function( self, target, file_list )
local odeps = self:create_compile_targets( file_list )
local exetarget = self:link_target( target, odeps )
self:default( self:add_target( exetarget ) )
return exetarget
end
-------------------------------------------------------------------------------
-- Other exported functions
function new_builder( build_dir )
return builder.new( build_dir )
end
local args = { ... }
local b = require "tools.build"
local builder = b.new_builder( ".build/cross-lua" )
local utils = b.utils
local sf = string.format
if not (_VERSION == "Lua 5.1" and pcall(require,"lfs")) then
print [[
cross_lua.lua must be run within Lua 5.1 and it requires the Lua Filesystem to be installed.
On most *nix distrubitions youwill find a packages lua-5.1 and lua-filesystem, or
alternalively you can install lua-rocks and use the Rocks package manager to install lfs.
]]
os.exit(1)
end
builder:init( args )
builder:set_build_mode( builder.BUILD_DIR_LINEARIZED )
local output = 'luac.cross'
local cdefs = '-DLUA_CROSS_COMPILER -Ddbg_printf=printf'
-- Lua source files and include path
local lua_files = [[
lapi.c lauxlib.c lbaselib.c lcode.c ldblib.c ldebug.c ldo.c ldump.c
lfunc.c lgc.c llex.c lmathlib.c lmem.c loadlib.c lobject.c lopcodes.c
lparser.c lrotable.c lstate.c lstring.c lstrlib.c ltable.c ltablib.c
ltm.c lundump.c lvm.c lzio.c
luac_cross/luac.c luac_cross/loslib.c luac_cross/print.c
../modules/linit.c
../libc/c_stdlib.c
]]
lua_files = lua_files:gsub( "\n" , "" )
local lua_full_files = utils.prepend_path( lua_files, "app/lua" )
local local_include = "-Iapp/include -Iinclude -Iapp/lua"
-- Compiler/linker options
builder:set_compile_cmd( sf( "gcc -O2 %s -Wall %s -c $(FIRST) -o $(TARGET)", local_include, cdefs ) )
builder:set_link_cmd( "gcc -o $(TARGET) $(DEPENDS) -lm" )
-- Build everything
builder:make_exe_target( output, lua_full_files )
builder:build()
...@@ -29,4 +29,8 @@ cd "$TRAVIS_BUILD_DIR"/ld || exit ...@@ -29,4 +29,8 @@ cd "$TRAVIS_BUILD_DIR"/ld || exit
cd "$TRAVIS_BUILD_DIR" || exit cd "$TRAVIS_BUILD_DIR" || exit
make clean make clean
make make
LUA_FILES=`find lua_modules lua_examples -iname "*.lua"`
echo checking $LUA_FILES
./luac.cross -p $LUA_FILES
) )
CC =gcc
SRCS=\ SRCS=\
main.c \ main.c \
../../app/spiffs/spiffs_cache.c ../../app/spiffs/spiffs_check.c ../../app/spiffs/spiffs_gc.c ../../app/spiffs/spiffs_hydrogen.c ../../app/spiffs/spiffs_nucleus.c ../../app/spiffs/spiffs_cache.c ../../app/spiffs/spiffs_check.c ../../app/spiffs/spiffs_gc.c ../../app/spiffs/spiffs_hydrogen.c ../../app/spiffs/spiffs_nucleus.c
......
...@@ -9,6 +9,8 @@ typedef int16_t s16_t; ...@@ -9,6 +9,8 @@ typedef int16_t s16_t;
typedef uint16_t u16_t; typedef uint16_t u16_t;
typedef int8_t s8_t; typedef int8_t s8_t;
typedef uint8_t u8_t; typedef uint8_t u8_t;
typedef long long ptrdiff_t;
#ifndef __CYGWIN__
typedef long long ptrdiff_t;
#define offsetof(type, member) __builtin_offsetof (type, member) #define offsetof(type, member) __builtin_offsetof (type, member)
#endif
-- Generic utility functions
module( ..., package.seeall )
local lfs = require "lfs"
local sf = string.format
-- Taken from Lake
dir_sep = package.config:sub( 1, 1 )
is_os_windows = dir_sep == '\\'
-- Converts a string with items separated by 'sep' into a table
string_to_table = function( s, sep )
if type( s ) ~= "string" then return end
sep = sep or ' '
if s:sub( -1, -1 ) ~= sep then s = s .. sep end
s = s:gsub( sf( "^%s*", sep ), "" )
local t = {}
local fmt = sf( "(.-)%s+", sep )
for w in s:gmatch( fmt ) do table.insert( t, w ) end
return t
end
-- Split a file name into 'path part' and 'extension part'
split_ext = function( s )
local pos
for i = #s, 1, -1 do
if s:sub( i, i ) == "." then
pos = i
break
end
end
if not pos or s:find( dir_sep, pos + 1 ) then return s end
return s:sub( 1, pos - 1 ), s:sub( pos )
end
-- Replace the extension of a given file name
replace_extension = function( s, newext )
local p, e = split_ext( s )
if e then
if newext and #newext > 0 then
s = p .. "." .. newext
else
s = p
end
end
return s
end
-- Return 'true' if building from Windows, false otherwise
is_windows = function()
return is_os_windows
end
-- Prepend each component of a 'pat'-separated string with 'prefix'
prepend_string = function( s, prefix, pat )
if not s or #s == 0 then return "" end
pat = pat or ' '
local res = ''
local st = string_to_table( s, pat )
foreach( st, function( k, v ) res = res .. prefix .. v .. " " end )
return res
end
-- Like above, but consider 'prefix' a path
prepend_path = function( s, prefix, pat )
return prepend_string( s, prefix .. dir_sep, pat )
end
-- full mkdir: create all the paths needed for a multipath
full_mkdir = function( path )
local ptables = string_to_table( path, dir_sep )
local p, res = ''
for i = 1, #ptables do
p = ( i ~= 1 and p .. dir_sep or p ) .. ptables[ i ]
res = lfs.mkdir( p )
end
return res
end
-- Concatenate the given paths to form a complete path
concat_path = function( paths )
return table.concat( paths, dir_sep )
end
-- Return true if the given array contains the given element, false otherwise
array_element_index = function( arr, element )
for i = 1, #arr do
if arr[ i ] == element then return i end
end
end
-- Linearize an array with (possibly) embedded arrays into a simple array
_linearize_array = function( arr, res, filter )
if type( arr ) ~= "table" then return end
for i = 1, #arr do
local e = arr[ i ]
if type( e ) == 'table' and filter( e ) then
_linearize_array( e, res, filter )
else
table.insert( res, e )
end
end
end
linearize_array = function( arr, filter )
local res = {}
filter = filter or function( v ) return true end
_linearize_array( arr, res, filter )
return res
end
-- Return an array with the keys of a table
table_keys = function( t )
local keys = {}
foreach( t, function( k, v ) table.insert( keys, k ) end )
return keys
end
-- Return an array with the values of a table
table_values = function( t )
local vals = {}
foreach( t, function( k, v ) table.insert( vals, v ) end )
return vals
end
-- Returns true if 'path' is a regular file, false otherwise
is_file = function( path )
return lfs.attributes( path, "mode" ) == "file"
end
-- Returns true if 'path' is a directory, false otherwise
is_dir = function( path )
return lfs.attributes( path, "mode" ) == "directory"
end
-- Return a list of files in the given directory matching a given mask
get_files = function( path, mask, norec, level )
local t = ''
level = level or 0
for f in lfs.dir( path ) do
local fname = path .. dir_sep .. f
if lfs.attributes( fname, "mode" ) == "file" then
local include
if type( mask ) == "string" then
include = fname:find( mask )
else
include = mask( fname )
end
if include then t = t .. ' ' .. fname end
elseif lfs.attributes( fname, "mode" ) == "directory" and not fname:find( "%.+$" ) and not norec then
t = t .. " " .. get_files( fname, mask, norec, level + 1 )
end
end
return level > 0 and t or t:gsub( "^%s+", "" )
end
-- Check if the given command can be executed properly
check_command = function( cmd )
local res = os.execute( cmd .. " > .build.temp 2>&1" )
os.remove( ".build.temp" )
return res
end
-- Execute a command and capture output
-- From: http://stackoverflow.com/a/326715/105950
exec_capture = function( cmd, raw )
local f = assert(io.popen(cmd, 'r'))
local s = assert(f:read('*a'))
f:close()
if raw then return s end
s = string.gsub(s, '^%s+', '')
s = string.gsub(s, '%s+$', '')
s = string.gsub(s, '[\n\r]+', ' ')
return s
end
-- Execute the given command for each value in a table
foreach = function ( t, cmd )
if type( t ) ~= "table" then return end
for k, v in pairs( t ) do cmd( k, v ) end
end
-- Generate header with the given #defines, return result as string
gen_header_string = function( name, defines )
local s = "// eLua " .. name:lower() .. " definition\n\n"
s = s .. "#ifndef __" .. name:upper() .. "_H__\n"
s = s .. "#define __" .. name:upper() .. "_H__\n\n"
for key,value in pairs(defines) do
s = s .. string.format("#define %-25s%-19s\n",key:upper(),value)
end
s = s .. "\n#endif\n"
return s
end
-- Generate header with the given #defines, save result to file
gen_header_file = function( name, defines )
local hname = concat_path{ "inc", name:lower() .. ".h" }
local h = assert( io.open( hname, "w" ) )
h:write( gen_header_string( name, defines ) )
h:close()
end
-- Remove the given elements from an array
remove_array_elements = function( arr, del )
del = istable( del ) and del or { del }
foreach( del, function( k, v )
local pos = array_element_index( arr, v )
if pos then table.remove( arr, pos ) end
end )
end
-- Remove a directory recusively
-- USE WITH CARE!! Doesn't do much checks :)
rmdir_rec = function ( dirname )
if lfs.attributes( dirname, "mode" ) ~= "directory" then return end
for f in lfs.dir( dirname ) do
local ename = string.format( "%s/%s", dirname, f )
local attrs = lfs.attributes( ename )
if attrs.mode == 'directory' and f ~= '.' and f ~= '..' then
rmdir_rec( ename )
elseif attrs.mode == 'file' or attrs.mode == 'named pipe' or attrs.mode == 'link' then
os.remove( ename )
end
end
lfs.rmdir( dirname )
end
-- Concatenates the second table into the first one
concat_tables = function( dst, src )
foreach( src, function( k, v ) dst[ k ] = v end )
end
-------------------------------------------------------------------------------
-- Color-related funtions
-- Currently disabled when running in Windows
-- (they can be enabled by setting WIN_ANSI_TERM)
local dcoltable = { 'black', 'red', 'green', 'yellow', 'blue', 'magenta', 'cyan', 'white' }
local coltable = {}
foreach( dcoltable, function( k, v ) coltable[ v ] = k - 1 end )
local _col_builder = function( col )
local _col_maker = function( s )
if is_os_windows and not os.getenv( "WIN_ANSI_TERM" ) then
return s
else
return( sf( "\027[%d;1m%s\027[m", coltable[ col ] + 30, s ) )
end
end
return _col_maker
end
col_funcs = {}
foreach( coltable, function( k, v )
local fname = "col_" .. k
_G[ fname ] = _col_builder( k )
col_funcs[ k ] = _G[ fname ]
end )
-------------------------------------------------------------------------------
-- Option handling
local options = {}
options.new = function()
local self = {}
self.options = {}
setmetatable( self, { __index = options } )
return self
end
-- Argument validator: boolean value
options._bool_validator = function( v )
if v == '0' or v:upper() == 'FALSE' then
return false
elseif v == '1' or v:upper() == 'TRUE' then
return true
end
end
-- Argument validator: choice value
options._choice_validator = function( v, allowed )
for i = 1, #allowed do
if v:upper() == allowed[ i ]:upper() then return allowed[ i ] end
end
end
-- Argument validator: choice map (argument value maps to something)
options._choice_map_validator = function( v, allowed )
for k, value in pairs( allowed ) do
if v:upper() == k:upper() then return value end
end
end
-- Argument validator: string value (no validation)
options._string_validator = function( v )
return v
end
-- Argument printer: boolean value
options._bool_printer = function( o )
return "true|false", o.default and "true" or "false"
end
-- Argument printer: choice value
options._choice_printer = function( o )
local clist, opts = '', o.data
for i = 1, #opts do
clist = clist .. ( i ~= 1 and "|" or "" ) .. opts[ i ]
end
return clist, o.default
end
-- Argument printer: choice map printer
options._choice_map_printer = function( o )
local clist, opts, def = '', o.data
local i = 1
for k, v in pairs( opts ) do
clist = clist .. ( i ~= 1 and "|" or "" ) .. k
if o.default == v then def = k end
i = i + 1
end
return clist, def
end
-- Argument printer: string printer
options._string_printer = function( o )
return nil, o.default
end
-- Add an option of the specified type
options._add_option = function( self, optname, opttype, help, default, data )
local validators =
{
string = options._string_validator, choice = options._choice_validator,
boolean = options._bool_validator, choice_map = options._choice_map_validator
}
local printers =
{
string = options._string_printer, choice = options._choice_printer,
boolean = options._bool_printer, choice_map = options._choice_map_printer
}
if not validators[ opttype ] then
print( sf( "[builder] Invalid option type '%s'", opttype ) )
os.exit( 1 )
end
table.insert( self.options, { name = optname, help = help, validator = validators[ opttype ], printer = printers[ opttype ], data = data, default = default } )
end
-- Find an option with the given name
options._find_option = function( self, optname )
for i = 1, #self.options do
local o = self.options[ i ]
if o.name:upper() == optname:upper() then return self.options[ i ] end
end
end
-- 'add option' helper (automatically detects option type)
options.add_option = function( self, name, help, default, data )
local otype
if type( default ) == 'boolean' then
otype = 'boolean'
elseif data and type( data ) == 'table' and #data == 0 then
otype = 'choice_map'
elseif data and type( data ) == 'table' then
otype = 'choice'
data = linearize_array( data )
elseif type( default ) == 'string' then
otype = 'string'
else
print( sf( "Error: cannot detect option type for '%s'", name ) )
os.exit( 1 )
end
self:_add_option( name, otype, help, default, data )
end
options.get_num_opts = function( self )
return #self.options
end
options.get_option = function( self, i )
return self.options[ i ]
end
-- Handle an option of type 'key=value'
-- Returns both the key and the value or nil for error
options.handle_arg = function( self, a )
local si, ei, k, v = a:find( "([^=]+)=(.*)$" )
if not k or not v then
print( sf( "Error: invalid syntax in '%s'", a ) )
return
end
local opt = self:_find_option( k )
if not opt then
print( sf( "Error: invalid option '%s'", k ) )
return
end
local optv = opt.validator( v, opt.data )
if optv == nil then
print( sf( "Error: invalid value '%s' for option '%s'", v, k ) )
return
end
return k, optv
end
-- Show help for all the registered options
options.show_help = function( self )
for i = 1, #self.options do
local o = self.options[ i ]
print( sf( "\n %s: %s", o.name, o.help ) )
local values, default = o.printer( o )
if values then
print( sf( " Possible values: %s", values ) )
end
print( sf( " Default value: %s", default or "none (changes at runtime)" ) )
end
end
-- Create a new option handler
function options_handler()
return options.new()
end
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment