diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index c56a73ec38b68ae38cb6d38130e2a66ffa155cdf..082ccae7fc5d91ca0a9e044a9b13d88b2045f9fd 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -4,7 +4,6 @@ image: docker:19.03.1 # improved performance. variables: DOCKER_DRIVER: overlay2 - CONTAINER_IMAGE: registry.gitlab.e.foundation:5000/e/os/lineageota PUBLISH_USER: root PUBLISH_URL: ota.ecloud.global DOCKER_TLS_CERTDIR: "/certs" @@ -15,11 +14,11 @@ services: build: stage: build script: - - docker pull $CONTAINER_IMAGE:$CI_COMMIT_REF_NAME || true - - docker login -u gitlab-ci-token -p $CI_BUILD_TOKEN registry.gitlab.e.foundation:5000 - - docker build --cache-from $CONTAINER_IMAGE:latest -t $CONTAINER_IMAGE -t $CONTAINER_IMAGE:$CI_COMMIT_REF_NAME . - - docker push $CONTAINER_IMAGE:$CI_COMMIT_REF_NAME - - if [ "${CI_COMMIT_REF_NAME}" = master ] ; then docker push $CONTAINER_IMAGE:latest ; fi + - docker pull $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_NAME || true + - docker login -u gitlab-ci-token -p $CI_BUILD_TOKEN $CI_REGISTRY + - docker build --cache-from $CI_REGISTRY_IMAGE:latest -t $CI_REGISTRY_IMAGE -t $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_NAME . + - docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_NAME + - if [ "${CI_COMMIT_REF_NAME}" = master ] ; then docker push $CI_REGISTRY_IMAGE:latest ; fi tags: - generic_privileged diff --git a/README.md b/README.md index d8d1bc420f8245cd6b431e0f469cfdbdb0f95cf7..542c44d7227dd6fd6a4a0a7dbbc147c5624dde9e 100644 --- a/README.md +++ b/README.md @@ -43,6 +43,21 @@ then finally visit http://localhost/ to see the REST Server up and running. - Full builds should be uploaded into `builds/full` directory. - Delta builds should be uploaded into `builds/delta` directory. +### Folder Structure for Builds + +All the builds , props, md5sum and other files of a device and channel needs to be in common folder. You must follow below folder structure. The same folder name must be used in API. + +```shell +builds/full// +``` + +For Example, All builds and other files of star2lte for stable channel will be in `/builds/full/stable/star2lte` + +### Device and Channel fields are now mandatory in API + +The fields `device` and `channel` are mandatory and `incremental` is an optional field in an API (which is used to get the builds). + + ## How to specify allowed migration paths - You can specify migration rules (whether an upgrade from a version to a version is allowed or not) by creating a migration_paths.json file in your build directory @@ -56,6 +71,30 @@ then finally visit http://localhost/ to see the REST Server up and running. - The contents should be read as: Upgrades from major version 9 to major version 9 and to major version 10 are allowed. Upgrades from major version 10 to major version 10 and to major version 11 are allowed. - Current version is always available as an upgrade (if a more recent build is found), even if file is not available (for backwards compatibility too). +## How to set incremental rollout percentage in build config file + +- You can specify incremental rollout rules by adding values in `config.json` file. You can add the file only if you want percentage incremental rollout for a build. +- The format of file name will be : `.zip.config.json` + +```shell +$ cd builds/full/test/guacamoleb/ +$ tree +. +├── e.x.xx-xxyyzz-guacamoleb.zip # the full ROM zip file +└── e.x.xx-xxyyzz-guacamoleb.zip.config.json # the ROM build.config.json file +``` + +- E.g. : Suppose builds for "guacamoleb" devices are stored at "/mnt/rom/builds/full/test/guacamoleb". The file "/mnt/rom/builds/full/test/guacamoleb/e.x.xx-xxyyzz-guacamoleb.zip.config.json" should then have contents like: +```json + { + "rollout": { + "percentage": 20 + } + } +``` +- The `percentage` value must be between 0 to 100. Do not add `%` sign after the value. +- The `config.json` file is not mandatory. In case it does not exist, a build will consider a 100% rollout percentage. + ### ONLY for LineageOS 15.x and newer If you are willing to use this project on top of your LineageOS 15.x ( or newer ) ROM builds, you may have noticed that the file named `build.prop` have been removed inside your ZIP file, and has been instead integrated within your `system.new.dat` file, which is basically an ext4 image ( you can find out more here: https://source.android.com/devices/tech/ota/block ). @@ -82,6 +121,23 @@ I am not sure how much this may help anyway, but this must be used as an extreme Feel free to use this [simple script](https://github.com/julianxhokaxhiu/LineageOTAUnitTest) made with NodeJS. Instructions are included. +## LineageOTA Unit Testing + +You can cerate new `{FILENAME}Test.php` in `tests` folder. Use `vendor/bin/phpunit tests` command in CLI to run the tests. +In every test file, in `setUp()` function, you must set below details for valid test results : + +```properties +$this->romType = '{device}'; +$this->deviceType = '{type}'; +$this->incrementalVersion = '{incr}'; + +{device} - Device name +{type} - Build type +{incr} - Incremental version + +``` + + ## How to integrate within your ROM In order to integrate this REST Server within your ROM you have two possibilities: you can make use of the `build.prop` ( highly suggested ), or you can patch directly the `android_packages_apps_CMUpdater` package ( not suggested ). diff --git a/composer.json b/composer.json index 6d66bcec1e42a6aa3bb6493a2ea855f7a0cc45c5..61e635892f2c94d9dce89832019f0de3082ffa75 100644 --- a/composer.json +++ b/composer.json @@ -57,5 +57,8 @@ }, "scripts": { "serve": "php -S 0.0.0.0:8000" + }, + "require-dev": { + "phpunit/phpunit": "^9" } } diff --git a/index.php b/index.php index 68d433923d79ffc0cf1518c357d20bf9e3a27c1a..0a159e1effdc4e8e85f8204bb482f111ce29af7e 100644 --- a/index.php +++ b/index.php @@ -60,5 +60,5 @@ $app = new CmOta($logger); $app - ->setConfig( 'basePath', $protocol.$_SERVER['HTTP_HOST'].dirname($_SERVER['SCRIPT_NAME']) ) + ->setConfig( 'basePath', rtrim( $protocol.$_SERVER['HTTP_HOST'].dirname($_SERVER['SCRIPT_NAME']) , "/" ) ) ->run(); diff --git a/src/CmOta.php b/src/CmOta.php index 8df8c54b2b3f8db3b2ec8bcd2f103aa663d037a9..d04591ff76dbaa7a71ab98bcaaf7a6893cb4d623 100644 --- a/src/CmOta.php +++ b/src/CmOta.php @@ -110,25 +110,26 @@ // LineageOS new API Flight::route('/api/v1/@deviceType/@romType(/@incrementalVersion)', function ( $deviceType, $romType, $incrementalVersion ){ - Flight::builds()->setPostData( - array( - 'params' => array( - 'device' => $deviceType, - 'channels' => array( - $romType, - ), - 'source_incremental' => $incrementalVersion, - ), - ) - ); - - $ret = array( - 'id' => null, - 'response' => Flight::builds()->get(), - 'error' => null - ); - - Flight::json($ret); + Flight::builds()->setPostData( + array( + 'params' => array( + 'device' => $deviceType, + 'channels' => array( + $romType, + ), + 'source_incremental' => $incrementalVersion, + ), + ) + ); + + $ret = array( + 'id' => null, + 'response' => Flight::builds()->get(), + 'error' => null + ); + + Flight::json($ret); + }); } diff --git a/src/Helpers/Build.php b/src/Helpers/Build.php index 307a76cfa5b91f86696e50c04129bd4d4ebd2886..e873d837b7269e8effc75ac0301d80303c31ed28 100644 --- a/src/Helpers/Build.php +++ b/src/Helpers/Build.php @@ -38,6 +38,7 @@ private $incremental = ''; private $filePath = ''; private $buildProp = ''; + private $confProp = ''; private $uid = null; private $size = ''; private $logger = null; @@ -82,6 +83,7 @@ // - builds/CURRENT_ZIP_FILE.zip/system/build.prop // - builds/CURRENT_ZIP_FILE.zip.prop ( which must exist ) $this->buildProp = explode("\n", @file_get_contents($this->getPropFilePath())); + $this->confProp = $this->getConfProp(); // Try to fetch build.prop values. In some cases, we can provide a fallback, in other a null value will be given $this->timestamp = intval($this->getBuildPropValue('ro.build.date.utc') ?? filemtime($this->filePath)); $this->incremental = $this->getBuildPropValue('ro.build.version.incremental') ?? ''; @@ -97,7 +99,7 @@ $this->url = $this->_getUrl('', Flight::cfg()->get('buildsPath')); } else { $this->url = $this->_getUrl('', Flight::cfg()->get('basePath') . substr($physicalPath, $position)); - } + } $this->changelogUrl = $this->_getChangelogUrl(); if (!file_exists($this->filePath . '.prop')) { $prop = ""; @@ -510,4 +512,49 @@ { return is_callable($func) && false === stripos(ini_get('disable_functions'), $func); } - } + + /** + * Check if the current build is valid with multiple conditions + * @param type $params The params dictionary inside the current POST request + * @return boolean True if valid, False if not. + */ + public function includeInResults($params) + { + return $this->isValid($params) && $this->checkRollout() && $this->getSize() > 0; + } + + /** + * Return the correct config file path (depending of version) + * @return string + */ + private function getConfigFilePath() + { + return file_exists($this->filePath . '.config.json') ? $this->filePath . '.config.json' : ''; + } + + /** + * Return the all values from config file path + * @return JSON if valid otherwise null + */ + private function getConfProp(){ + $configFilePath = $this->getConfigFilePath(); + return ($configFilePath) ? json_decode( file_get_contents($configFilePath) , true) : array(); + } + + /** + * Return if rollout successful or not for a build + * @return boolean + */ + public function checkRollout() + { + $rolloutpercentage = isset($this->confProp['rollout']['percentage']) ? (int) $this->confProp['rollout']['percentage'] : 100; + if ($rolloutpercentage <= 0 || $rolloutpercentage > 100) { + return TRUE; + } + $rand_number = rand(1, 100); + if ($rand_number > $rolloutpercentage) { + return FALSE; + } + return TRUE; + } + } \ No newline at end of file diff --git a/src/Helpers/Builds.php b/src/Helpers/Builds.php index b85773bd3d2904f5472a9b7fbce0a64914ce465e..f4cdbe6d41e07db0cdfae5aae21035e480be372a 100644 --- a/src/Helpers/Builds.php +++ b/src/Helpers/Builds.php @@ -203,17 +203,20 @@ } else { $build = new Build($file, $dir, $this->logger); } - - if ($build->isValid($this->postData['params'])) { + + if ($build->includeInResults($this->postData['params'])) { array_push($this->builds, $build); - if (!empty($this->postData['params']) && strcmp($this->postData['params']['source_incremental'], $build->getIncremental()) == 0) { - $this->currentBuild = $build; - $this->logger->info($build->getIncremental().' is the current build'); - } + } + + $sourceIncremental = isset($this->postData['params']['source_incremental']) ? $this->postData['params']['source_incremental'] : NULL; + if ($build->isValid($this->postData['params']) && $sourceIncremental && strcmp($sourceIncremental, $build->getIncremental()) === 0) { + $this->currentBuild = $build; + $this->logger->info($build->getIncremental().' is the current build'); } } } } $this->logger->debug('Total execution time of getBuilds in seconds'); } - } + +} \ No newline at end of file diff --git a/tests/ApiTest.php b/tests/ApiTest.php new file mode 100755 index 0000000000000000000000000000000000000000..07990aad22f0abbe3f396d37cf0f2fb74542696e --- /dev/null +++ b/tests/ApiTest.php @@ -0,0 +1,81 @@ +router = new Router(); + $this->request = new Request(); + $this->dispatcher = new Dispatcher(); + + $this->romType = 'dev'; + $this->deviceType = 'Amber'; + $this->incrementalVersion = '0b971351f3'; + + $this->customData = array( + 'params' => array( + 'device' => $this->deviceType, + 'channels' => array( + $this->romType, + ), + 'source_incremental' => $this->incrementalVersion, + ), + ); + + $this->logger = new Logger('main'); + $this->getCmotaInstance(); + } + + public function getCmotaInstance(){ + Flight::register('cmota', '\JX\CmOta\CmOta',array($this->logger)); + return Flight::cmota(); + } + public function getBuildInstance(){ + Flight::register('builds', '\JX\CmOta\Helpers\Builds',array($this->logger)); + return Flight::builds(); + } + + public function testCheckIfBuildValid() { + $builds = $this->getBuildInstance(); + $builds->setPostData($this->customData); + $records = $builds->get(); + $this->assertGreaterThan( 0, sizeof($records),'Builds are not valid!'); + } + + public function testCheckIfCorrectAttribute() { + $attributes = array("timestamp" , "md5sum" , "url"); // You can change attributes here + + $builds = $this->getBuildInstance(); + $builds->setPostData($this->customData); + $records = $builds->get(); + if(sizeof($records)){ + foreach ($attributes as $attribute) { + $this->assertArrayHasKey($attribute, $records[0], "Builds doesn't contains '".$attribute."' as key"); + } + }else{ + $this->assertFalse('Builds'); + } + } + + +} \ No newline at end of file diff --git a/tests/BuildTest.php b/tests/BuildTest.php new file mode 100755 index 0000000000000000000000000000000000000000..71a9b61765867bfd59595ad98a7fda438b65fd9a --- /dev/null +++ b/tests/BuildTest.php @@ -0,0 +1,82 @@ +router = new Router(); + $this->request = new Request(); + $this->dispatcher = new Dispatcher(); + + $this->romType = 'dev'; + $this->deviceType = 'Amber'; + $this->incrementalVersion = '0b971351f3'; + + $this->customData = array( + 'params' => array( + 'device' => $this->deviceType, + 'channels' => array( + $this->romType, + ), + 'source_incremental' => $this->incrementalVersion, + ), + ); + + $this->logger = new Logger('main'); + $this->getCmotaInstance(); + } + + public function getCmotaInstance(){ + Flight::register('cmota', '\JX\CmOta\CmOta',array($this->logger)); + return Flight::cmota(); + } + public function getBuildInstance(){ + Flight::register('builds', '\JX\CmOta\Helpers\Builds',array($this->logger)); + return Flight::builds(); + } + + // newer than (timestamp check) + public function testCheckTimestamp() { + $builds = $this->getBuildInstance(); + $builds->setPostData($this->customData); + $records = $builds->get(); + $currentTimestamp = strtotime(date('Y-m-d H:i:s')); + + foreach ($records as $record) { + $this->assertGreaterThan($record['timestamp'], $currentTimestamp, 'Build is not new!'); + } + + } + + // check Android version if it is same or not + public function testCheckAndroidVersion() { + $builds = $this->getBuildInstance(); + $builds->setPostData($this->customData); + $records = $builds->get(); + $expectedVersion = '0.16'; + foreach ($records as $record) { + $this->assertEquals($expectedVersion , $record['version'] ,$expectedVersion.' & '.$record['version'] .' Android Versions are different!'); + } + } + +} \ No newline at end of file