diff --git a/README.md b/README.md index 65da41a..fe41d3b 100644 --- a/README.md +++ b/README.md @@ -39,6 +39,48 @@ It provides several grunt task: } ``` +- `sectv-deploy`: Deploys packaged (`.zip` or `.wgt`) Cordova apps to supported smart tv platforms through their relative deploy methods. + + - Options for the task: + + ```js + 'sectv-deploy': { // task + 'sectv-orsay': { // target + dest: 'platforms/sectv-orsay/build/', // Path to use for host server root + id: 'app', // Widget Identifier + port: '80', // Port of the host server (default 80) + zipName: 'app.zip' // Name of the packaged app archive + /* + After the host server has started, boot up your TV in developer mode, point it at the correct IP address, and sync your apps. + */ + }, + 'sectv-tizen': { + wgtPath: 'app.wgt', // Path of the packaged widget + /* + Deployment is performed via: 'sdb install /' + */ + }, + 'tv-webos': { + ipkPath: 'platform/webos/build/app.ipk', // Path of the packaged app archive + device: 'emulator', // Target device name + /* + Deployment is performed via: ares-install --device / + */ + } + } + ``` + +When deploying to Samsung Orsay TVs via HTTP, the device expects a specific folder layout and manifest file: +/ +├── widgetlist.xml +└── Widget/ + └── app.zip + +- widgetlist.xml must reside in the server root and describe the available widget(s). +- The actual app package (app.zip) must be placed inside the Widget/ subdirectory. +- The field in widgetlist.xml must point to http:///Widget/app.zip. +This structure allows Orsay TVs in developer mode to retrieve and install the app over the network without proprietary tools. + # Associated Projects - [cordova-sectv-orsay](http://github.com/Samsung/cordova-sectv-orsay) is an application library that allows for [Cordova](http://cordova.apache.org)-based projects to be built for the Legacy Samsung Smart TV (A.K.A Orsay) Platform. diff --git a/package.json b/package.json index e858a14..6c8e5a0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "grunt-cordova-sectv", - "description": "Grunt task for build and package the cordova project with cordova-sectv-*** platforms", + "description": "Grunt task for build, package and deploy the cordova project with cordova-sectv-*** platforms", "version": "1.4.1", "author": { "name": "Samsung Electronics", @@ -18,11 +18,13 @@ "grunt-contrib-jshint": "^0.11.2", "inquirer": "^5.2.0", "js2xmlparser": "^1.0.0", + "xmlbuilder2": "^3.0.2", "mkdirp": "^1.0.3", "mustache": "^2.3.2", "shelljs": "^0.5.3", "xml2js": "^0.4.23", - "zip-dir": "^1.0.2" + "zip-dir": "^1.0.2", + "express": "^4.18.2" }, "peerDependencies": { "grunt": ">=0.4.0" diff --git a/sample/Gruntfile.js b/sample/Gruntfile.js index afa8718..beb38f7 100644 --- a/sample/Gruntfile.js +++ b/sample/Gruntfile.js @@ -61,6 +61,17 @@ module.exports = function(grunt) { www: 'platforms/tv-webos/www', dest: 'platforms/tv-webos/build' } + }, + 'sectv-deploy': { + 'sectv-orsay': { + dest: 'platforms/sectv-orsay/build' + }, + 'sectv-tizen': { + dest: 'platforms/sectv-tizen/build' + }, + 'tv-webos': { + dest: 'platforms/tv-webos/build' + } } }); @@ -76,6 +87,7 @@ module.exports = function(grunt) { 'jshint', 'clean', 'sectv-prepare', - 'sectv-build' + 'sectv-build', + 'sectv-deploy' ]); }; diff --git a/tasks/packager/sectv-orsay.js b/tasks/packager/sectv-orsay.js index 8ae1c35..33707ea 100644 --- a/tasks/packager/sectv-orsay.js +++ b/tasks/packager/sectv-orsay.js @@ -25,7 +25,9 @@ var shelljs = require('shelljs'); var mustache = require('mustache'); var grunt = require('grunt'); var zipdir = require('zip-dir'); -var js2xmlparser = require('js2xmlparser'); +var xml2js = require('xml2js'); +var os = require('os'); +var express = require('express'); var revLen = 3; @@ -316,14 +318,19 @@ function prepareDir(dir) { mkdirp.sync(dir); } -function getManualOrsayConfData(platformsData){ - var i, - manualOrsayConfData = null; +function getManualOrsayConfData(platformsData) { + var manualOrsayConfData = null; + var builder = new xml2js.Builder({ + headless: true, + renderOpts: { pretty: false } + }); + if (platformsData) { - for (i=0; i < platformsData.length; i++) { + for (var i = 0; i < platformsData.length; i++) { if (platformsData[i].$.name === 'sectv-orsay') { delete platformsData[i].$; - manualOrsayConfData = utils.trim(js2xmlparser('platform',platformsData[i],{declaration : {include : false},attributeString : '$'}).replace(/<(\/?platform)>/igm,'')); + var xml = builder.buildObject(platformsData[i]); + manualOrsayConfData = utils.trim(xml); } } } @@ -493,5 +500,162 @@ module.exports = { successCallback && successCallback(); }); }); + }, + deploy: function (successCallback, errorCallback, data) { // HTTP host server + widgetlist.xml + console.log('\nStart deploying Legacy Samsung Smart TV Platform apps......'); + var dest = data.dest || path.join('platforms', 'sectv-orsay', 'build'); + dest = path.resolve(dest); + var serverRoot = data.serverRoot || '../serverRoot'; + serverRoot = path.resolve(serverRoot) + var port = data.port || 80; + + var userConfPath = path.join('platforms', 'userconf.json'); + + var projectName = 'package'; + + if(fs.existsSync(userConfPath)){ + var userData = JSON.parse(fs.readFileSync(userConfPath)); + + if(userData.hasOwnProperty('orsay')){ + projectName = userData.orsay.name; + } + } + else { + grunt.log.error('Prepare and Build the project first.'); + } + + var appId = data.id || projectName; + var zipName = data.zipName || projectName + '.zip'; + + arrangeFolders(serverRoot, function (err) { + if (err) { + grunt.log.error(err); + errorCallback && errorCallback(err); + return; + } + + updateWidgetList(serverRoot, function (err) { + if (err) { + grunt.log.error(err); + errorCallback && errorCallback(err); + return; + } + + startServer(serverRoot, port); + grunt.log.ok(`Host Server started at ${serverRoot}`); + successCallback && successCallback(); + }); + }); + + function getNetworkIp() { + var interfaces = os.networkInterfaces(); + for (var name of Object.keys(interfaces)) { + for (var intf of interfaces[name]) { + if (intf.family === 'IPv4' && !intf.internal) { + return intf.address; + } + } + } + return '127.0.0.1'; + } + + function arrangeFolders(serverRoot, cb) { + fs.mkdir(path.join(serverRoot, 'Widget'), { recursive: true }, function (err) { + if (err) return cb(err); + + var srcZip = path.join(dest, zipName); + var destZip = path.join(serverRoot, 'Widget', zipName); + + fs.copyFile(srcZip, destZip, function (err) { + if (err) return cb(err); + grunt.log.ok(`Copied ${zipName} to ${destZip}`); + cb(null); + }); + }); + } + + function updateWidgetList(serverRoot, cb) { + var xmlPath = path.resolve(path.join(serverRoot, 'widgetlist.xml')); + var builder = new xml2js.Builder({ headless: false, renderOpts: { pretty: true } }); + + if (fs.existsSync(xmlPath)) { + var existing = fs.readFileSync(xmlPath, 'utf8'); + var parser = new xml2js.Parser({ explicitArray: false }); + + parser.parseString(existing, (err, obj) => { + if (err) return cb(err); + + if (!obj.rsp) obj.rsp = { $: { stat: 'ok' }, list: { widget: [] } }; + if (!obj.rsp.list) obj.rsp.list = { widget: [] }; + if (!Array.isArray(obj.rsp.list.widget)) { + obj.rsp.list.widget = obj.rsp.list.widget ? [obj.rsp.list.widget] : []; + } + + // Search if there is already a widget with the same id + var existingIndex = obj.rsp.list.widget.findIndex(w => w.$.id === appId); + + var newWidget = { + $: { id: appId }, + title: projectName, + compression: { $: { size: '12345', type: 'zip' } }, + description: 'My TOAST App -- Description', + download: `http://${getNetworkIp()}/Widget/${zipName}` + }; + + if (existingIndex >= 0) { + // Update existing + obj.rsp.list.widget[existingIndex] = newWidget; + grunt.log.ok(`Updated existing widget ${appId}`); + } else { + // Add new + obj.rsp.list.widget.push(newWidget); + grunt.log.ok(`Added new widget ${appId}`); + } + + var xml = builder.buildObject(obj); + fs.writeFile(xmlPath, xml, err => { + if (err) return cb(err); + grunt.log.ok(`Manifest written at ${xmlPath}`); + cb(null); + }); + }); + } else { + var obj = { + rsp: { + $: { stat: 'ok' }, + list: { + widget: [{ + $: { id: appId }, + title: projectName, + compression: { $: { size: '12345', type: 'zip' } }, + description: 'My Samsung Application -- Description', + download: `http://${getNetworkIp()}/Widget/${zipName}` + }] + } + } + }; + + var xml = builder.buildObject(obj); + fs.writeFile(xmlPath, xml, err => { + if (err) return cb(err); + grunt.log.ok(`Created new manifest at ${xmlPath}`); + cb(null); + }); + } + } + + function startServer(serverRoot, port) { + var app = express(); + app.all('*', (req, res, next) => { + res.header('Access-Control-Allow-Origin', '*'); + res.header('Access-Control-Allow-Headers', 'X-Requested-With'); + next(); + }); + app.use(express.static(serverRoot)); + app.listen(port, () => { + grunt.log.ok(`Server root at ${serverRoot}/`); + grunt.log.ok(`Server running at http://${getNetworkIp()}:${port}/`); + }); + } } -}; +}; \ No newline at end of file diff --git a/tasks/packager/sectv-tizen.js b/tasks/packager/sectv-tizen.js index 1e00299..4c0fb75 100644 --- a/tasks/packager/sectv-tizen.js +++ b/tasks/packager/sectv-tizen.js @@ -368,5 +368,40 @@ module.exports = { } } }); + }, + deploy: function (successCallback, errorCallback, data) { // Smart Developement Bridge + console.log('\nStart deploying Tizen Samsung Smart TV Platform apps......'); + + var { spawn } = require('child_process'); + var wgtPath = data.wgtPath || './platform/tizen/build/app.wgt'; + + var userConfPath = path.join('platforms', 'userconf.json'); + + var projectName = 'package'; + + if(fs.existsSync(userConfPath)){ + var userData = JSON.parse(fs.readFileSync(userConfPath)); + + if(userData.hasOwnProperty('tizen')){ + projectName = userData.tizen.name; + } + } + else { + grunt.log.error('Prepare and Build the project first.'); + } + + var proc = spawn('sdb', ['install', wgtPath]); + + proc.stdout.on('data', d => grunt.log.write(d.toString())); + proc.stderr.on('data', d => grunt.log.error(d.toString())); + proc.on('close', code => { + if (code === 0) { + grunt.log.ok('Tizen app deployed successfully.'); + successCallback && successCallback(); + } else { + grunt.log.error('Tizen app deployment failed.'); + errorCallback && errorCallback(); + } + }); } }; diff --git a/tasks/packager/tv-webos.js b/tasks/packager/tv-webos.js index cecadbc..0e8a983 100644 --- a/tasks/packager/tv-webos.js +++ b/tasks/packager/tv-webos.js @@ -421,5 +421,41 @@ module.exports = { } } }); + }, + deploy: function (successCallback, errorCallback, data) { // WebOS CLI + console.log('\nStart deploying WebOS Smart TV Platform apps......'); + + var { spawn } = require('child_process'); + var ipkPath = data.ipkPath || './platform/webos/build/app.ipk'; + var device = data.device || 'emulator'; + + var userConfPath = path.join('platforms', 'userconf.json'); + + var projectName = 'package'; + + if(fs.existsSync(userConfPath)){ + var userData = JSON.parse(fs.readFileSync(userConfPath)); + + if(userData.hasOwnProperty('tizen')){ + projectName = userData.tizen.name; + } + } + else { + grunt.log.error('Prepare and Build the project first.'); + } + + var proc = spawn('ares-install', ['--device', device, ipkPath]); + + proc.stdout.on('data', d => grunt.log.write(d.toString())); + proc.stderr.on('data', d => grunt.log.error(d.toString())); + proc.on('close', code => { + if (code === 0) { + grunt.log.ok('WebOS app deployed successfully.'); + successCallback && successCallback(); + } else { + grunt.log.error('WebOS app deployment failed.'); + errorCallback && errorCallback(); + } + }); } }; diff --git a/tasks/sectv-deploy.js b/tasks/sectv-deploy.js new file mode 100644 index 0000000..d51dbb8 --- /dev/null +++ b/tasks/sectv-deploy.js @@ -0,0 +1,33 @@ +/* + * Copyright 2015 Samsung Electronics Co., Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +'use strict'; + +module.exports = function(grunt) { + grunt.registerMultiTask('sectv-deploy', 'Deploy app to SmartTV', function () { + var platformName = this.target; + + var packager = require('./packager/'+platformName); + var done = this.async(); + + // deploy: function(successCallback, errorCallback, data) + packager.deploy(function () { + // server waiting for client + }, function () { + done(false); + }, this.data); + }); +};